Валидация в доменном дизайне - PullRequest
58 голосов
/ 05 февраля 2009

Как вы справляетесь с проверкой сложных агрегатов в доменном дизайне? Консолидируете ли вы свои бизнес-правила / логику проверки?

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

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

Ответы [ 5 ]

56 голосов
/ 07 февраля 2009

Вместо того чтобы полагаться на 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

38 голосов
/ 13 июня 2009

Мне нравится решение Джимми Богарда этой проблемы. В своем блоге у него есть пост под названием «Проверка сущности с помощью посетителей и методов расширения» , в котором он предлагает очень элегантный подход к проверке сущности, который предлагает реализацию отдельного класса для хранения кода проверки.

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;
    }
}
6 голосов
/ 07 февраля 2009

Я обычно использую класс спецификации, он предоставляет метод (это 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. Я помещаю эти классы в пространство имен модели с именами, которые предлагают их использование.

0 голосов
/ 13 марта 2018

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

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

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

// 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);
    }
}
0 голосов
/ 23 августа 2016

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

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

Пример 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
    }
}

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

...