Валидация в доменном дизайне
Как вы справляетесь с проверкой сложных агрегатов в доменном дизайне? Объединяете ли вы свои бизнес-правила / логику валидации?
Я понимаю аргументацию проверки. И я понимаю валидацию свойств, которую можно прикрепить к самим моделям, и делаю такие вещи, как проверка правильности адреса электронной почты или почтового индекса или минимальной и максимальной длины имени.
Но как насчет сложной проверки, которая включает несколько моделей? Где вы обычно размещаете эти правила и методы в своей архитектуре? И какие шаблоны, если таковые имеются, вы используете для их реализации?
5 ответов
Мне нравится решение Джимми Богарда этой проблемы. У него есть пост в своем блоге под названием "Проверка сущности с помощью посетителей и методов расширения", в котором он представляет очень элегантный подход к проверке сущности, который предлагает реализацию отдельного класса для хранения кода проверки.
public interface IValidator<T>
{
bool IsValid(T entity);
IEnumerable<string> BrokenRules(T entity);
}
public class OrderPersistenceValidator : IValidator<Order>
{
public bool IsValid(Order entity)
{
return BrokenRules(entity).Count() == 0;
}
public IEnumerable<string> BrokenRules(Order entity)
{
if (entity.Id < 0)
yield return "Id cannot be less than 0.";
if (string.IsNullOrEmpty(entity.Customer))
yield return "Must include a customer.";
yield break;
}
}
Вместо того чтобы полагаться на IsValid(xx)
звонки по всей вашей заявке, рассмотрите возможность получить совет от Грега Янга:
Никогда не позволяйте вашим сущностям входить в недопустимое состояние.
В основном это означает, что вы перешли от представления о сущностях как о чистых контейнерах данных и больше об объектах с поведением.
Рассмотрим пример адреса человека:
person.Address = "123 my street";
person.City = "Houston";
person.State = "TX";
person.Zip = 12345;
Между любыми из этих вызовов ваша сущность недействительна (потому что у вас есть свойства, которые не согласуются друг с другом. Теперь рассмотрим следующее:
person.ChangeAddress(.......);
все вызовы, связанные с изменением адреса, теперь являются атомарной единицей. Ваша сущность никогда не является недействительной здесь.
Если вы берете эту идею моделирования поведения, а не состояния, то вы можете достичь модели, которая не допускает недопустимых объектов.
Для хорошего обсуждения этого, проверьте это интервью infoq: http://www.infoq.com/interviews/greg-young-ddd
Обычно я использую спецификационный класс, он предоставляет метод (это C#, но вы можете перевести его на любой язык):
bool IsVerifiedBy(TEntity candidate)
Этот метод выполняет полную проверку кандидата и его отношений. Вы можете использовать аргументы в классе спецификации, чтобы сделать его параметризованным, например, проверить уровень...
Вы также можете добавить метод, чтобы узнать, почему кандидат не проверил спецификацию:
IEnumerable<string> BrokenRules(TEntity canditate)
Вы можете просто решить реализовать первый метод следующим образом:
bool IsVerifiedBy(TEntity candidate)
{
return BrokenRules(candidate).IsEmpty();
}
Для нарушенных правил я обычно пишу итератор:
IEnumerable<string> BrokenRules(TEntity candidate)
{
if (someComplexCondition)
yield return "Message describing cleary what is wrong...";
if (someOtherCondition)
yield return
string.Format("The amount should not be {0} when the state is {1}",
amount, state);
}
Для локализации вы должны использовать ресурсы, и почему бы не передать культуру в метод BrokenRules. Я помещаю эти классы в пространство имен модели с именами, которые предлагают их использование.
Многократная проверка модели должна проходить через ваш сводный корень. Если вам нужно проверить через совокупные корни, у вас, вероятно, есть недостаток дизайна.
Способ, которым я делаю проверку для агрегатов, заключается в том, чтобы вернуть интерфейс ответа, который сообщает мне, прошла ли проверка / нет, и любые сообщения о том, почему она не прошла.
Вы можете проверить все подмодели в совокупном корне, чтобы они оставались согласованными.
// Command Response class to return from public methods that change your model
public interface ICommandResponse
{
CommandResult Result { get; }
IEnumerable<string> Messages { get; }
}
// The result options
public enum CommandResult
{
Success = 0,
Fail = 1
}
// My default implementation
public class CommandResponse : ICommandResponse
{
public CommandResponse(CommandResult result)
{
Result = result;
}
public CommandResponse(CommandResult result, params string[] messages) : this(result)
{
Messages = messages;
}
public CommandResponse(CommandResult result, IEnumerable<string> messages) : this(result)
{
Messages = messages;
}
public CommandResult Result { get; private set; }
public IEnumerable<string> Messages { get; private set; }
}
// usage
public class SomeAggregateRoot
{
public string SomeProperty { get; private set; }
public ICommandResponse ChangeSomeProperty(string newProperty)
{
if(newProperty == null)
{
return new CommandResponse(CommandResult.Fail, "Some property cannot be changed to null");
}
SomeProperty = newProperty;
return new CommandResponse(CommandResult.Success);
}
}
Этот вопрос уже устарел, но на случай, если кому-то будет интересно, вот как я реализую проверку в моих классах обслуживания.
У меня есть частный метод Validate в каждом из моих классов обслуживания, который принимает экземпляр сущности и выполняемое действие, если в случае неудачной проверки возникает пользовательское исключение с деталями нарушенных правил.
Пример DocumentService со встроенной проверкой
public class DocumentService : IDocumentService
{
private IRepository<Document> _documentRepository;
public DocumentService(IRepository<Document> documentRepository)
{
_documentRepository = documentRepository;
}
public void Create(Document document)
{
Validate(document, Action.Create);
document.CreatedDate = DateTime.Now;
_documentRepository.Create(document);
}
public void Update(Document document)
{
Validate(document, Action.Update);
_documentRepository.Update(document);
}
public void Delete(int id)
{
Validate(_documentRepository.GetById(id), Action.Delete);
_documentRepository.Delete(id);
}
public IList<Document> GetAll()
{
return _documentRepository
.GetAll()
.OrderByDescending(x => x.PublishDate)
.ToList();
}
public int GetAllCount()
{
return _documentRepository
.GetAll()
.Count();
}
public Document GetById(int id)
{
return _documentRepository.GetById(id);
}
// validation
private void Validate(Document document, Action action)
{
var brokenRules = new List<string>();
if (action == Action.Create || action == Action.Update)
{
if (string.IsNullOrWhiteSpace(document.Title))
brokenRules.Add("Title is required");
if (document.PublishDate == null)
brokenRules.Add("Publish Date is required");
}
if (brokenRules.Any())
throw new EntityException(string.Join("\r\n", brokenRules));
}
private enum Action
{
Create,
Update,
Delete
}
}
Мне нравится этот подход, потому что он позволяет мне собрать всю логику проверки ядра в одном месте, что упрощает задачу.