Где мы должны поставить проверку для модели домена

38

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

public class Order
 {
    private readonly List<OrderLine> _lineItems;

    public virtual Customer Customer { get; private set; }
    public virtual DateTime OrderDate { get; private set; }
    public virtual decimal OrderTotal { get; private set; }

    public Order (Customer customer)
    {
        if (customer == null)
            throw new  ArgumentException("Customer name must be defined");

        Customer = customer;
        OrderDate = DateTime.Now;
        _lineItems = new List<LineItem>();
    }

    public void AddOderLine //....
    public IEnumerable<OrderLine> AddOderLine { get {return _lineItems;} }
}


public class OrderLine
{
    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }
    public virtual int Quantity { get; set; }
    public virtual decimal UnitPrice { get; set; }

    public OrderLine(Order order, int quantity, Product product)
    {
        if (order == null)
            throw new  ArgumentException("Order name must be defined");
        if (quantity <= 0)
            throw new  ArgumentException("Quantity must be greater than zero");
        if (product == null)
            throw new  ArgumentException("Product name must be defined");

        Order = order;
        Quantity = quantity;
        Product = product;
    }
}

Спасибо за все ваши предложения.

adisembiring
источник

Ответы:

47

Есть интересная статья Мартина Фаулера на эту тему, которая выделяет аспект, который большинство людей (включая меня) склонны упускать из виду:

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

Я думаю, что гораздо полезнее думать о проверке как о чем-то, что связано с контекстом - обычно это действие, которое вы хотите выполнить. Является ли этот заказ действительным для заполнения, является ли этот клиент действительным для регистрации в отеле. Поэтому вместо того, чтобы иметь такие методы, как isValid, есть такие методы, как isValidForCheckIn.

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

Опять из статьи:

В «About Face» Алан Купер заявил, что мы не должны допускать, чтобы наши представления о допустимых состояниях мешали пользователю вводить (и сохранять) неполную информацию. Это напомнило мне несколько дней назад, когда я читал черновик книги, над которой работает Джимми Нильссон. Он сформулировал принцип, согласно которому вы всегда должны сохранять объект, даже если в нем есть ошибки. Хотя я не уверен, что это должно быть абсолютным правилом, я думаю, что люди склонны предотвращать сбережения больше, чем следовало бы. Размышление о контексте проверки может помочь предотвратить это.

Майкл Боргвардт
источник
Слава богу, кто-то сказал это. Формы, которые содержат 90% данных, но ничего не сохраняют, несправедливы по отношению к пользователям, которые часто составляют остальные 10%, просто чтобы не потерять данные, поэтому все, что нужно сделать для проверки, - это заставить систему потерять отслеживание, 10% был составлен. Подобные проблемы могут возникнуть в бэкэнде - скажем, при импорте данных. Я обнаружил, что, как правило, лучше попытаться правильно работать с неверными данными, чем пытаться предотвратить их появление.
PSR
2
@psr Вам даже нужна внутренняя логика, если ваши данные не сохраняются? Вы можете оставить все манипуляции на стороне клиента, если ваши данные не имеют никакого значения для вашей бизнес-модели. Было бы также бесполезной тратой ресурсов на отправку сообщений туда и обратно (клиент-сервер), если данные бессмысленны. Итак, мы возвращаемся к идее «никогда не позволять объектам вашего домена входить в недопустимое состояние!» ,
Geo C.
2
Интересно, почему так много голосов за такой неоднозначный ответ. При использовании DDD иногда существуют некоторые правила, которые просто проверяют, являются ли некоторые данные INT или находятся в диапазоне. Например, когда вы позволяете пользователю вашего приложения выбирать некоторые ограничения для его продуктов (сколько раз кто-то может предварительно просмотреть мой продукт и в каких интервалах дней месяца). Здесь оба ограничения должны быть int и одно из них должно быть в диапазоне 0-31. Кажется, это проверка формата данных, которая в среде без DDD подходит для службы или контроллера. Но в DDD я поддерживаю валидацию в домене (90%).
Geo C.
2
Принуждение верхних уровней знать слишком много о домене для поддержания его в действительном состоянии пахнет как плохой плохой дизайн. Домен должен быть тем, который гарантирует, что его состояние будет действительным. Слишком сильное перемещение по плечам верхних слоев может привести к анемии в вашем домене, и вы можете преодолеть некоторые сдерживающие ограничения, которые могут повредить вашему бизнесу. Теперь я понимаю, что правильным обобщением было бы сохранение вашей проверки как можно ближе к вашей персистентности, насколько это возможно, или к вашему коду манипулирования данными (когда он манипулируется для достижения конечного состояния).
Geo C.
PS Я не смешиваю авторизацию (разрешено что-то делать), аутентификацию (сообщение пришло из правильного места или было отправлено правильным клиентом, оба идентифицируются с помощью ключа API / токена / имени пользователя или чего-либо еще) с проверкой формата или бизнес-правила. Когда я говорю 90%, я имею в виду те бизнес-правила, которые в большинстве из них также включают проверку формата. Разумеется, проверка формата может осуществляться на верхних уровнях, но большинство из них будут находиться в домене (даже формат адреса электронной почты, который будет проверен в объекте значения EmailAddress).
Geo C.
6

Несмотря на то, что этот вопрос немного устарел, я хотел бы добавить кое-что стоящее:

Я хотел бы согласиться с @MichaelBorgwardt и расширить его, подняв тестируемость. В книге «Эффективная работа с устаревшим кодом» Майкл Фезерс много говорит о препятствиях для тестирования, и одно из этих препятствий - это «трудно построить» объекты. Построение недопустимого объекта должно быть возможным, и, как предполагает Фаулер, зависящие от контекста проверки достоверности должны быть в состоянии идентифицировать эти условия. Если вы не можете понять, как создать объект в тестовой системе, у вас будут проблемы с тестированием вашего класса.

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

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

Павел
источник
1
Много раз объекты кажутся сложными для постройки. Например, в этом случае вы можете обойти открытый конструктор, создав класс Wrapper, который наследуется от тестируемого класса и позволяет вам создать экземпляр базового объекта в недопустимом состоянии. Именно здесь использование правильных модификаторов доступа для классов и конструкторов вступает в игру и может действительно нанести ущерб тестированию при неправильном использовании. Кроме того, избегание «запечатанных» классов и методов, за исключением случаев, когда это необходимо, значительно облегчит тестирование кода.
П. Роу
4

Как я уверен, вы уже знаете ...

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

Проверка достоверности данных, передаваемых в качестве параметров c'tor, определенно действительна в конструкторе - в противном случае вы, возможно, разрешаете создание недопустимого объекта.

Однако (и это только мое мнение, на данный момент я не могу найти хороших документов) - если проверка данных требует сложных операций (таких как асинхронные операции - возможно, проверка на основе сервера при разработке приложения для настольного компьютера), тогда лучше вставьте функцию инициализации или явной проверки некоторого вида, и члены nullустановят значения по умолчанию (такие как ) в c'or.


Кроме того, как примечание, как вы включили его в свой пример кода ...

Если вы не выполняете дальнейшую проверку (или другую функциональность) AddOrderLine, я бы, скорее всего, представил List<LineItem>объект как свойство, а не выступил бы Orderкак фасад .

Демиан Брехт
источник
Зачем выставлять контейнер? Какое значение имеет верхний слой для контейнера? Вполне разумно иметь AddLineItemметод. На самом деле, для DDD это предпочтительнее. Если List<LineItem>изменить на пользовательский объект коллекции, то открытое свойство и все, что зависело от List<LineItem>свойства, подвержены изменениям, ошибкам и исключениям.
IAbstract
4

Проверка должна быть выполнена как можно скорее.

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

Исходя из вашего вопроса, я думаю, что ответ будет разделить проверку.

  1. Проверка свойства проверяет, является ли значение этого свойства правильным, например, когда диапазон от 1 до 10 исключен.

  2. Проверка объекта гарантирует, что все свойства объекта действительны в сочетании друг с другом. например, BeginDate до EndDate. Предположим, что вы читаете значение из хранилища данных, и BeginDate и EndDate инициализируются в DateTime.Min по умолчанию. При установке BeginDate нет никаких оснований для применения правила «must be before EndDate», так как это не применяется к YET. Это правило следует проверять ПОСЛЕ того, как были установлены все свойства. Это можно вызвать на уровне совокупного корня

  3. Валидация также должна быть предварительно сформирована для совокупного (или совокупного корневого) объекта. Объект Order может содержать действительные данные, как и его OrderLines. Но затем бизнес-правило гласит, что ни один заказ не может превышать 1000 долларов. Как вы будете применять это правило, в некоторых случаях это разрешено. Вы не можете просто добавить свойство «не проверять сумму», так как это приведет к злоупотреблению (рано или поздно, возможно, даже вы, просто чтобы убрать этот «неприятный запрос»).

  4. Далее идет проверка на уровне представления. Вы действительно собираетесь отправить объект по сети, зная, что он потерпит неудачу? Или вы сэкономите пользователю этот бурдон и сообщите ему, как только он введет недопустимое значение. Например, в большинстве случаев ваша среда разработки будет работать медленнее, чем производство. Хотели бы вы подождать 30 секунд, прежде чем вас проинформируют о том, что «вы забыли это поле СНОВА во время еще ДРУГОГО тестового прогона», особенно когда есть ошибка в работе, которая должна быть исправлена, когда босс дышит вам в шею?

  5. Предполагается, что проверка на уровне постоянства должна быть максимально приближена к проверке значения свойства. Это поможет предотвратить исключения при чтении ошибок «null» или «invalid value» при использовании картографов любого типа или простых старых считывателей данных. Использование хранимых процедур решает эту проблему, но требует написать ту же логику оценки СНОВА и выполнить ее СНОВА. А хранимые процедуры - это область администрирования БД, поэтому не пытайтесь также выполнять ЕГО работу (или, что еще хуже, беспокоите его этой «мелкой добычей, за которую ему не платят»).

так сказать с некоторыми известными словами «это зависит», но по крайней мере теперь вы знаете, ПОЧЕМУ это зависит.

Хотелось бы разместить все это в одном месте, но, к сожалению, этого сделать нельзя. Выполнение этого приведет к зависимости от «объекта Бога», содержащего ВСЕ проверки для ВСЕХ слоев. Вы не хотите идти по этому темному пути.

По этой причине я только выкидываю исключения проверки уровня свойства. На всех других уровнях я использую ValidationResult с методом IsValid, чтобы собрать все «нарушенные правила» и передать их пользователю в одном AggregateException.

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

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

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

Если вам интересно, почему у меня также есть проверка на уровне агрегации (или, если хотите, на уровне обслуживания), это потому, что у меня нет хрустального шара, говорящего мне, кто будет использовать мои услуги в будущем. У вас будет достаточно проблем с поиском собственных ошибок, чтобы другие люди не допускали ошибок ваших, введя неверные данные. Например, вы управляете приложением A, но приложение B передает некоторые данные, используя ваш сервис. Угадай, кого они спрашивают первыми, когда есть ошибка? Администратор приложения B с радостью сообщит пользователю «с моей стороны нет ошибок, я просто передаю данные».

Уэсли Кенис
источник