Где должна существовать логика проверки?

Вопрос

Как лучше всего я могу управлять построением графа объектов, где требуется сложная логика проверки?. Я хотел бы сохранить зависимые инъецированные конструкторы do-nothing для целей проверки.

Testability очень важна для меня, что делает ваше предложение для поддержания этого атрибута кода?

Фон

У меня есть объект plain-old-java-object, который управляет структурой некоторых бизнес-данных для меня:

class Pojo
{
    protected final String p;

    public Pojo(String p) {
        this.p = p;
    }
}

Я хочу убедиться, что p имеет допустимый формат, потому что этот бизнес-объект действительно не имеет смысла без этой гарантии; его даже не нужно создавать, если р - бессмыслица. Однако проверка p не является тривиальной.

Catch

Действительно, для этого требуется достаточно сложная логика проверки, что логика должна быть полностью проверена в ней собственными правами, и поэтому у меня есть эта логика в отдельном классе:

final class BusinessLogic() implements Validator<String>
{
    public String validate(String p) throws InvalidFoo, InvalidBar {
        ...
    }
}

Возможные повторяющиеся вопросы

  • Где должна быть реализована логика проверки? - принятый ответ непроницаем для меня. Я прочитал "быть запущенным в родной среде класса" в качестве тавтологии, как правила проверки могут выполняться в чем-либо, кроме "родной среды класса"? Точка 2 я не могу заглянуть, поэтому я не могу комментировать.
  • Где предоставить логические правила для проверки? - Оба ответа предполагают, что ответственность лежит на поставщике клиент/данные, который мне нравится в принципе. Однако в моем случае клиент может не быть инициатором данных и не может его проверить.
  • Где сохранить логику проверки? - предполагает, что проверка может принадлежать модели, однако я считаю этот подход менее идеальным для тестирования. В частности, для каждого unit test мне нужно заботиться обо всей логике проверки даже во время тестирования других частей модели - я никогда не могу полностью изолировать то, что я хочу проверить, следуя предложенному подходу.

Мое мышление до сих пор

Хотя следующий конструктор открыто объявляет зависимости Pojo и сохраняет свою простоту проверки, он совершенно небезопасен. Здесь нет ничего, что помешало бы клиенту предоставить валидатор, который утверждает, что каждый вход является приемлемым!

public Pojo(Validator businessLogic, String p) throws InvalidFoo, InvalidBar {
    this.p = businessLogic.validate(p);
}

Итак, я немного ограничиваю видимость конструктора, и я предоставляю метод factory, который обеспечивает проверку, а затем конструкцию:

@VisibleForTesting
Pojo(String p) {
    this.p = p;
}

public static Pojo createPojo(String p) throws InvalidFoo, InvalidBar {
    Validator businessLogic = new BusinessLogic();
    businessLogic.validate(p);
    return new Pojo(p);
}

Теперь я мог бы реорганизовать createPojo в класс factory, который восстановит "единую ответственность" для Pojo и упростит тестирование логики factory, не говоря уже о преимуществах производительности, которые больше не расточительно создают новый (без гражданства) BusinessLogic для каждого нового Pojo.

Мне кажется, что я должен остановиться, попросить внешнее мнение. Я на правильном пути?

Ответ 1

Несколько элементов ответа ниже... Дайте мне знать, если это имеет смысл и отвечает на ваши вопросы.


Введение. Я считаю, что ваша система может быть простой библиотекой, многоуровневым приложением или сложной распределенной системой, на самом деле не имеет большого значения, когда дело доходит до проверки:

  • клиент: удаленный клиент (например, HTTP-клиент) или просто другой класс, вызывающий вашу библиотеку.
  • : удаленная служба (например, служба REST) ​​или то, что вы открываете для своего API.

Где проверить?

Обычно вы хотите проверить свои входные параметры:

  • на стороне клиент, прежде чем передавать параметры службе, чтобы гарантировать, что на этом объекте будут действовать действительные строки. Это особенно желательно, если это удаленная служба или существует сложный поток между генерацией параметров и созданием объекта.

  • на стороне службы:

    • на уровне класса, в вашем конструкторе, чтобы обеспечить создание достоверных объектов;
    • на уровне подсистемы, то есть слой, управляющий этими объектами (например, DAL, сохраняющий ваши Pojo s);
    • на границе вашего сервиса, например. facade или внешний API вашей библиотеки или вашего контроллера (как в MVC, например, конечная точка REST, Spring и т.д.).

Как проверить?

Предполагая вышеизложенное, поскольку вам может понадобиться повторно использовать логику проверки в нескольких местах, может быть хорошей идеей извлечь его в класс . Таким образом:

  • вы не дублируете его (DRY!);
  • вы уверены, что каждый уровень системы будет проверять то же самое;
  • вы можете легко unit test использовать эту логику отдельно (потому что она без гражданства).

Более конкретно, вы бы назвали эту логику в своем конструкторе, чтобы обеспечить справедливость объекта (подумайте о наличии действительных зависимостей в качестве предварительных условий для ваших алгоритмов, представленных в методах Pojo):

Класс полезности:

public final class PojoValidator() {
    private PojoValidator() {
        // Pure utility class, do NOT instantiate.
    }

    public static String validateFoo(final String foo) {
        // Validate the provided foo here.
        // Validation logic can throw:
        // - checked exceptions if you can/want to recover from an invalid foo, 
        // - unchecked exceptions if you consider these as runtime exceptions and can't really recover (less clutering of your API).
        return foo;
    }

    public static Bar validateBar(final Bar bar) {
        // Same as above...
        // Can itself call other validators.
        return bar;
    }
}

Класс Pojo: Обратите внимание на инструкцию статического импорта для лучшей удобочитаемости.

import static PojoValidator.validateFoo;
import static PojoValidator.validateBar;

class Pojo {
    private final String foo;
    private final Bar bar;

    public Pojo(final String foo, final Bar bar) {
        validateFoo(foo);
        validateBar(bar);
        this.foo = foo;
        this.bar = bar;
    }
}

Как проверить мое Pojo?

  • Вы должны добавить тесты модульного уровня, чтобы убедиться, что ваша логика проверки вызывается во время построения (чтобы избежать регрессий, поскольку люди "упрощают" ваш конструктор позже, удалив эту логику проверки по причинам X, Y, Z).

  • Встраивайте создание зависимостей, если они просты, так как это делает ваш тест более читабельным, поскольку все, что вы используете, является локальным (меньше прокрутки, меньшего размера и т.д.)

  • Однако, если настройка ваших зависимостей Pojo достаточно сложна/достаточно много, чтобы тест более не читался, вы можете определить эту настройку в методе @Before/setUp, чтобы модульные тесты Тестирование логики Pojo действительно сосредоточено на проверке поведения вашего Pojo.

В любом случае, я согласен с Джеффом Стори:

  • напишите ваши тесты с допустимыми параметрами,
  • не имеют конструктора без проверки только для ваших тестов. Это действительно запах кода, когда вы смешиваете код производства и тестирования и определенно будете использоваться непреднамеренно кем-то в какой-то момент, включая стабильность вашего сервиса.

Наконец, подумайте о своих тестах как примеры кода, примеры или исполняемые спецификации: вы не хотите приводить путаный пример:

  • ввод неверных параметров;
  • вводя валидатор, который загромождает ваш API/читает "странно".

Что делать, если Pojo требует ОЧЕНЬ сложных зависимостей?

[Если это так, сообщите нам об этом]

Производственный код: Вы можете попытаться скрыть эту сложность в factory.

Код тестирования: Или:

  • как упомянуто выше, умножите это на другой метод; или
  • используйте factory; или
  • использовать параметры mock и настраивать их только для того, чтобы они проверяли требования для прохождения ваших тестов.

EDIT: несколько ссылок о проверке ввода в контексте безопасности, которые также могут быть полезны:

Ответ 2

Во-первых, я бы сказал, что логика проверки не должна жить исключительно в клиенте. Это поможет вам не помещать неверные данные в хранилище данных. Если вы добавляете дополнительных клиентов (возможно, у вас толстый клиент, и вы, например, добавляете клиент веб-службы, вам необходимо поддерживать проверку в обоих местах).

Я не думаю, что у вас должен быть конструктор для построения объекта, который не проверяет (с помощью этой @VisibleForTesting аннотации). Обычно вы должны тестировать действительные объекты (если вы не проверяете случаи ошибок). Кроме того, добавление дополнительного кода в ваш производственный код, который предназначен только для тестирования, - это запах кода, поскольку он не является действительно производственным кодом.

Я думаю, что подходящее место для логики проверки находится внутри самого объекта домена. Это гарантирует, что вы не создадите недопустимый объект домена.

Мне не очень нравится идея передачи валидатора в объект домена. Это много работает на клиентах домена, которые должны знать о валидаторах. Если вы хотите создать отдельный валидатор, который может добавить преимущества повторного использования и модуляции, но я бы не стал это вводить. При тестировании вы всегда можете использовать макет объекта, который полностью обходит проверку.

Добавление проверки в модель домена является чем-то общим, что делают веб-фреймворки (grails/rails). Я считаю, что это хороший подход, и это не должно мешать тестированию.

Ответ 3

Я хочу убедиться, что p имеет допустимый формат, потому что этот бизнес-объект действительно не имеет смысла без этой гарантии; его даже не следует создавать, если p - бессмыслица. Однако проверка p является нетривиальной.

  • "p допустимого формата" означает, что тип p не только String, но и более конкретный тип.

Например, EmailString:

class Pojo
{
    protected final EmailString p;

    public Pojo(EmailString p) {
        this.p = p;
    }
}
  1. Должен быть преобразователь между String и EmailString. Этот конвертер, являющийся частью бизнес-логики, гарантирует, что p не может быть недопустимым. Всякий раз, когда потребитель имеет String и хочет создать Pojo, он использует этот конвертер.

Существует несколько вариантов реализации этого конвертера для удобного захвата неконвертируемых случаев, например, .net style:

class Converter
{
    public static bool TryParse(String s, out EmailString p) {

    }
}

Testability очень важна для меня, что делает ваше предложение для поддержания этого атрибута кода?

Таким образом вы можете протестировать логику Pojo отдельно от логики разбора/преобразования String.