Как отделить проверку данных от моих простых доменных объектов (POCO)?
Этот вопрос не зависит от языка, но я парень на C#, поэтому я использую термин POCO для обозначения объекта, который только преформирует хранилище данных, обычно используя поля getter и setter.
Я просто переделал мою модель предметной области, чтобы она была суперсовременной POCO, и у меня осталась пара проблем относительно того, как обеспечить, чтобы значения свойств имели смысл в домене.
Например, Конечная дата Сервиса не должна превышать Конечную дату Контракта, на котором находится Сервис. Тем не менее, это похоже на нарушение SOLID, чтобы поставить проверку в установщик Service.EndDate, не говоря уже о том, что по мере того, как количество проверок, которые необходимо выполнить, мои классы POCO будут загромождены.
У меня есть некоторые решения (буду публиковать ответы), но у них есть свои недостатки, и мне интересно, каковы некоторые из любимых подходов к решению этой дилеммы?
8 ответов
Я думаю, что вы исходите из неверного предположения, то есть, что у вас должны быть объекты, которые ничего не делают, кроме хранения данных, и не имеют никаких методов, кроме методов доступа. Весь смысл наличия объектов заключается в инкапсуляции данных и поведения. Если у вас есть вещь, которая, по сути, просто структура, какое поведение вы инкапсулируете?
Я всегда слышу от людей аргументы в пользу метода "Validate" или "IsValid".
Лично я думаю, что это может сработать, но в большинстве проектов DDD вы обычно получаете несколько проверок, которые допустимы в зависимости от конкретного состояния объекта.
Поэтому я предпочитаю "IsValidForNewContract", "IsValidForTermination" или аналогичные, потому что я считаю, что большинство проектов имеют несколько таких валидаторов / состояний на класс. Это также означает, что у меня нет интерфейса, но я могу написать агрегированные валидаторы, которые очень хорошо читают, отражают условия бизнеса, которые я утверждаю.
Я действительно верю, что универсальные решения в этом случае очень часто отвлекают внимание от того, что важно - что делает код - для очень незначительного выигрыша в технической элегантности (интерфейс, делегат или что-то еще). Просто проголосуй за это;)
У моего коллеги возникла идея, которая сработала довольно хорошо. Мы никогда не придумали для него отличного названия, но назвали его "Инспектор / Судья".
Инспектор будет смотреть на объект и сообщать вам все правила, которые он нарушил. Судья решит, что с этим делать. Это разделение позволило нам сделать пару вещей. Это позволило нам разместить все правила в одном месте (инспектор), но мы могли бы иметь несколько судей и выбирать судью по контексту.
Один пример использования нескольких судей вращается вокруг правила, согласно которому Клиент должен иметь адрес. Это было стандартное трехуровневое приложение. На уровне пользовательского интерфейса судья выдаст что-то, что пользовательский интерфейс сможет использовать для указания полей, которые необходимо заполнить. Судья пользовательского интерфейса не выбрасывает исключения. На уровне обслуживания был еще один судья. Если во время сохранения он найдет клиента без адреса, он выдаст исключение. В этот момент вы действительно должны остановить процесс.
У нас также были судьи, которые были более строгими, так как состояние объектов менялось. Это была заявка на страхование, и в процессе цитирования полису разрешалось сохранять в неполном состоянии. Но как только эта Политика была готова к тому, чтобы стать Активизированной, многое нужно было установить. Таким образом, судья-заявитель со стороны службы был не так строг, как судья-активатор. Тем не менее, правила, используемые в Инспекторе, были все те же, так что вы все равно могли сказать, что было неполным, даже если вы решили ничего с этим не делать.
Одно из решений состоит в том, чтобы каждый объект DataAccessObject получал список валидаторов. Когда вызывается Save, он выполняет проверку каждого валидатора:
public class ServiceEndDateValidator : IValidator<Service> {
public void Check(Service s) {
if(s.EndDate > s.Contract.EndDate)
throw new InvalidOperationException();
}
}
public class ServiceDao : IDao<Service> {
IValidator<Service> _validators;
public ServiceDao(IEnumerable<IValidator<Service>> validators) {_validators = validators;}
public void Save(Service s) {
foreach(var v in _validators)
v.Check(service);
// Go on to save
}
}
Преимущество, очень понятный SoC, недостаток в том, что мы не получим чек, пока не будет вызван Save().
В прошлом я обычно делегировал валидацию сервису, такому как ValidationService. Это в принципе все еще доходит до философии DDD.
Внутренне это будет содержать коллекцию Validators и очень простой набор открытых методов, таких как Validate(), которые могут возвращать коллекцию объекта ошибки.
Очень просто, как-то так в C#
public class ValidationService<T>
{
private IList<IValidator> _validators;
public IList<Error> Validate(T objectToValidate)
{
foreach(IValidator validator in _validators)
{
yield return validator.Validate(objectToValidate);
}
}
}
Валидаторы могут быть либо добавлены в конструктор по умолчанию, либо внедрены через какой-либо другой класс, такой как ValidationServiceFactory.
Вот еще одна возможность. Проверка выполняется через прокси или декоратор объекта Domain:
public class ServiceValidationProxy : Service {
public override DateTime EndDate {
get {return EndDate;}
set {
if(value > Contract.EndDate)
throw new InvalidOperationexception();
base.EndDate = value;
}
}
}
Преимущество: мгновенная проверка. Может быть легко настроен через IoC.
Недостаток: если прокси, проверенные свойства должны быть виртуальными, если в декораторе все доменные модели должны быть основаны на интерфейсе. Классы проверки окажутся немного тяжелыми - прокси должны наследовать класс, а декораторы должны реализовать все методы. Наименование и организация могут запутаться.
Еще одна возможность - реализовать каждый из моих классов.
public interface Validatable<T> {
public event Action<T> RequiresValidation;
}
И пусть каждый установщик для каждого класса поднимает событие перед установкой (возможно, я мог бы добиться этого с помощью атрибутов).
Преимущество - проверка в реальном времени. Но более сложный код и неясно, кто должен делать присоединение.
Я думаю, что это, пожалуй, лучшее место для логики, на самом деле, но это только я. У вас может быть какой-то метод IsValid, который также проверяет все условия и возвращает true / false, может быть, это своего рода коллекция ErrorMessages, но это сомнительная тема, поскольку сообщения об ошибках на самом деле не являются частью модели предметной области. Я немного предвзят, поскольку я проделал некоторую работу с RoR, и это, по сути, то, что делают его модели.