DDD - Как избежать дублирования правил валидации

DDD - Как избежать дублирования правил валидации
DDD - Как избежать дублирования правил валидации - vdphotography @ Unsplash

Различные проверки существуют по разным причинам, поэтому существует несколько видов проверки. Если бы цель была точно такой же, не было бы смысла иметь обе эти эквивалентные проверки.

Давайте использовать пример здесь, это моя модель (и DTO и т. д.):

public class Foo
{
    public Guid CountryId { get; set; }
    public int? Amount { get; set; }
}

Вы можете предположить одну и ту же структуру данных на каждом обсуждаемом слое.

Короче говоря, это необязательные (и, следовательно, обнуляемые) значения, которые применяются к конкретной команде. Мы собираемся сосредоточиться на 4 видах проверки: внешний интерфейс, серверная часть (API), приложение и домен.

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

Домен

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

  • Одним из примеров может быть то, что сумма требуется для определенных стран, а не для других, или что вы можете взять сумму X только тогда, когда в этой стране есть >= X количество доступных единиц.

заявка

На уровне приложения вы захотите проверить, что ваши входные данные имеют смысл. Дело не в правилах, а в проверке работоспособности на наличие очевидных неверных данных.

Одна из распространенных проверок, которые вы видите здесь, — это сопоставление входных значений с хранилищем данных, чтобы увидеть, совпадают ли они (где это уместно).

  • Существует ли CountryId GUID в нашем списке стран?

API

По своей сути ваш API преобразует входящий веб-запрос в поведение вашего приложения. Поэтому здесь вы хотите проверить, правильно ли API сопоставляет входящий запрос с DTO вашего приложения.

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

  • Является ли значение CountryId, которое вы получаете, фактически значением GUID?

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

Вот список того, какие неверные данные будут обнаружены при какой проверке:

  • CountryId
    • fe2fc0e2-cfe8-4418-a7de-bbd459112229 (существует в базе данных)
      • Проверка API: проходит
      • Проверка приложения: проходит
      • Проверка домена: проходит
    • fe2fc0e2-cfe8-4418-a7de-bbd459112229 (существует в базе данных, но активы страны заморожены и не могут быть изъяты)
      • Проверка API: проходит
      • Проверка приложения: проходит
      • Проверка домена: не удается
    • 860fa9c6-c01c-48d4-ac3d-6b26a8085b24 (нет в базе)
      • Проверка API: проходит
      • Проверка приложения: не проходит
      • Проверка домена: нет данных
    • bananabanana (мусорные данные)
      • Проверка API: не удается
      • Проверка приложения: нет данных
      • Проверка домена: нет данных
  • Amount
    • 5 (меньше единиц в стране)
      • Проверка API: проходит
      • Проверка приложения: проходит
      • Проверка домена: проходит
    • 10 (более единиц в стране)
      • Проверка API: проходит
      • Проверка приложения: проходит
      • Проверка домена: не удается
    • -5 (явно бессмысленные данные, хотя это значение типа int)
      • Проверка API: проходит
      • Проверка приложения: не проходит
      • Проверка домена: нет данных
    • bananabanana (не целое число)
      • Проверка API: не удается
      • Проверка приложения: нет данных
      • Проверка домена: нет данных

Внешний интерфейс

Наконец, мы переходим к проверке внешнего интерфейса. Это немного странный зверь. С технической точки зрения вам не нужна проверка внешнего интерфейса. Бэкэнд-слои уже ловят все. Когда вы проходите внутренние проверки, вы уже знаете, что ваши входные данные:

  • [API] ... имеет соответствующий тип и соответствует приложению
  • [Приложение] ... имеет смысл и относится к существующим элементам (где это уместно)
  • [Домен]... соответствует всем известным бизнес-правилам

Итак, зачем нам нужна проверка интерфейса?

Основное внимание здесь уделяется пользовательскому опыту. До сих пор мы действительно проверяли только в интересах владельцев приложений, чтобы убедиться, что ничего плохого не происходит.

1
Приходилось ли вам когда-нибудь заполнять большую форму, может быть, даже ту, где вам нужно вводить сложные данные, которые вы должны прочитать из документов, которые у вас есть, только для того, чтобы отправить их, а затем вам сказали, что одно из этих сложных значений не хватает персонажа? Это очень расстраивает. Теперь мне нужно снова найти документ и снова прочитать сложные данные.

Если бы система немедленно предупредила меня об этом, когда документ все еще находился передо мной, это было бы намного приятнее для меня как для пользователя.

2
Может быть, я пытался взять больше предметов из определенной страны, чем было доступно. Но если единственный способ узнать это для меня — после того, как я нажму кнопку «Отправить», это означает, что мне уже нужно было подготовиться, чтобы взять предметы.

Например, если я хочу купить 5 билетов на концерт, я потрачу время на проверку того, есть ли у меня 4 друга, которые хотели бы пойти со мной на концерт. Если бы я знал, что доступно только 2 билета, мне не нужно было бы приглашать последних 3 друзей, которых я теперь должен отменить.

Ладно, может быть, экран уже показывал, что в наличии только два билета, но я мог это пропустить. Когда я ввел «5» в поле суммы, было бы неплохо, если бы система предупредила меня о моей ошибке.

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

Я использовал эти три примера как случаи, когда взаимодействие с пользователем может быть значительно улучшено за счет проверки внешнего интерфейса перед отправкой формы, потому что они отражают уровни проверки внутреннего интерфейса. 1 фактически совпадает с проверкой приложения (не соответствует известному идентификатору), 2 — проверкой домена (нарушает правила), а 3 — проверкой API (несовместимые данные, которые не сопоставляются).

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

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

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

Ваш вопрос

Допустим, у меня есть объект-значение Latitude, значение которого должно быть между -90 и 90. Нужно ли мне проверять это в конструкторе объекта-значения, а также на уровне приложения при преобразовании чего-то вроде LatitudeDTO в Latitude?

Модель предметной области обязательно должна подтверждать это.

Учитывая, что модель предметной области уже подтверждает это, какие дополнительные преимущества дает обнаружение той же ошибки на более раннем уровне? Теперь необходимость поддерживать две проверки связана с затратами, но в чем польза? Затраты без выгоды не имеют смысла.

Однако проверка внешнего интерфейса — это другой зверь. Здесь вам может понадобиться продублированная проверка входных значений просто для того, чтобы вам не приходилось обращаться к серверной части, прежде чем вы обнаружите, что ваши входные значения неверны.


Обновления из комментариев

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

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

Плохой пользовательский ввод будет обрабатываться прикладным уровнем.

«Плохой пользовательский ввод» расплывчат. Например, если кто-то пытается купить алкоголь в вашем интернет-магазине и правильно указывает, что родился в 2009 году, это неплохой пользовательский ввод. Все реквизиты указаны верно.

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

который отвечает за создание исключений с красивыми сообщениями, которые позже сопоставляются с HTTP 400.

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

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

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

Рассмотрите возможность использования объектов результата. FluentResults — хороший пример того, как можно справляться со сбоями, не прибегая к исключениям.

если прикладной уровень начинает зависеть от проверки домена для проверки ввода пользователя

Если проверка приложения зависит от проверки домена, то она может быть проверена только доменом. Ответственность приложения за проверку по своей сути определяется как ответственность, которая еще не охвачена доменом.

Если вы говорите о проверке приложения в зависимости от домена (домена, а не проверки домена!), то это может быть контекстуально нормально. Прикладным уровням, очевидно, разрешено обрабатывать объекты домена, поэтому я не вижу здесь проблем.

не всегда может быть сопоставлен с чем-то, что указывает на ошибку пользователя

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

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

Это очень контекстуально и не может быть помещено в общее заявление. Я не собираюсь преподносить вам догму. Это требует оценки и рассмотрения.

Например, сбой проверки домена может привести к тому, что прикладной уровень должен будет выйти (таким образом передав свой собственный вид сбоя API), или, возможно, прикладной уровень имеет резервный протокол, который можно использовать в случае, если первый подход не работает. .

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

уровень приложения должен будет перехватить это исключение и обосновать его, чтобы сообщить пользователю, что он ввел недопустимое значение для Latitude, поскольку исключение является общим и не обязательно означает ошибку пользователя (Latitude может быть создан службой, которая запрашивает данные из другой системы, поэтому исключение также может быть выдано по чужой вине)

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

public Result<LatLon> GetAntipodeanPoint(double lat, double lon)
{
    LatLon latLon;
    try
    {
        latLon = new LatLon(lat, lon);
    }
    catch(...)
    {
        return Result<LatLon>.Fail("The user is at fault");
    }

    LatLon antipodeanPoint;
    try
    {
        antipodeanPoint = someOtherService.GetAntipodeanPoint(latLon);
    }
    catch(...)
    {
        return Result<LatLon>.Fail("The service is at fault");
    }

    return Result.Ok(antipodeanPoint);
}

В общем, именно из-за такого рода проблем мне не нравится проверка конструктора, поскольку она не позволяет корректно обрабатывать ошибки проверки.

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

Прикрепляю к посту несколько видео по теме:

Прикрепленное видео 1 - Валидация в DDD – Константин Густов

Прикрепленное видео 2 - Владимир Хориков — Validaton and DDD

Прикрепленное видео 3 - Domain Driven Design – просто о сложном. Дмитрий Науменко


LetsCodeIt, 27 января 2023 г., 10:19