Где поставить проверку глобальных правил в DDD


Я новичок в DDD, и я пытаюсь применить его в реальной жизни. Нет никаких вопросов о такой логике проверки, как проверка null, проверка пустых строк и т. д. - Это напрямую относится к конструктору/свойству сущности. Но где поставить проверку некоторых глобальных правил, таких как "уникальное имя пользователя"?

Итак, у нас есть entity User

public class User : IAggregateRoot
{
   private string _name;

   public string Name
   {
      get { return _name; }
      set { _name = value; }
   }

   // other data and behavior
}

и репозиторий для пользователей

public interface IUserRepository : IRepository<User>
{
   User FindByName(string name);
}

опции:

  1. внедрить репозиторий в сущность
  2. внедрить репозиторий на завод
  3. создать операцию на доменной службе
  4. ???

и каждый вариант более подробно:

1 .Внедрить репозиторий в сущность

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

public User(IUserRepository repository)
{
    _repository = repository;
}

public string Name
{
    get { return _name; }
    set 
    {
       if (_repository.FindByName(value) != null)
          throw new UserAlreadyExistsException();

       _name = value; 
    }
}

обновление: мы можем использовать DI, чтобы скрыть зависимость между Пользователем и IUserRepository через объект спецификации.

2. Внедрить репозиторий на завод

Я могу поместить эту логику проверки в UserFactory. Но что если мы хотим изменить имя уже существующего пользователя?

3. Создать операцию на доменной службе

Я могу создать доменный сервис для создания и редактирования пользователей. Но кто-то может напрямую редактировать имя пользователя без вызова этой службы...

public class AdministrationService
{
    private IUserRepository _userRepository;

    public AdministrationService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public void RenameUser(string oldName, string newName)
    {
        if (_userRepository.FindByName(newName) != null)
            throw new UserAlreadyExistException();

        User user = _userRepository.FindByName(oldName);
        user.Name = newName;
        _userRepository.Save(user);
    }
}

4. ???

куда вы помещаете глобальную логику проверки для сущностей?

спасибо!

9 57

9 ответов:

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

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

основной заботой субъектов должно быть отслеживание состояния бизнеса - это достаточно ответственности, и они не должны быть связаны с проверкой.

пример

public class User
{
    public string Id { get; set; }
    public string Name { get; set; }
}

две спецификации:

public class IdNotEmptySpecification : ISpecification<User>
{
    public bool IsSatisfiedBy(User subject)
    {
        return !string.IsNullOrEmpty(subject.Id);
    }
}


public class NameNotTakenSpecification : ISpecification<User>
{
    // omitted code to set service; better use DI
    private Service.IUserNameService UserNameService { get; set; } 

    public bool IsSatisfiedBy(User subject)
    {
        return UserNameService.NameIsAvailable(subject.Name);
    }
}

и валидатор:

public class UserPersistenceValidator : IValidator<User>
{
    private readonly IList<ISpecification<User>> Rules =
        new List<ISpecification<User>>
            {
                new IdNotEmptySpecification(),
                new NameNotEmptySpecification(),
                new NameNotTakenSpecification()
                // and more ... better use DI to fill this list
            };

    public bool IsValid(User entity)
    {
        return BrokenRules(entity).Count() > 0;
    }

    public IEnumerable<string> BrokenRules(User entity)
    {
        return Rules.Where(rule => !rule.IsSatisfiedBy(entity))
                    .Select(rule => GetMessageForBrokenRule(rule));
    }

    // ...
}

для полноты интерфейсы:

public interface IValidator<T>
{
    bool IsValid(T entity);
    IEnumerable<string> BrokenRules(T entity);
}

public interface ISpecification<T>
{
    bool IsSatisfiedBy(T subject);
}

Примечания

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

ссылки

связанный вопрос с хорошим ответом с примером:проверка в доменном дизайне.

Эрик Эванс описывает использование шаблона спецификации для проверки, выбора и построения объекта в Глава 9, стр. 145.

этой статья по спецификации pattern С приложением в .Net может представлять интерес для вас.

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

Джимми Нильссон в своем "применении доменного дизайна и шаблонов" рекомендует проверять для конкретной операции, а не только для сохранения. Хотя сущность может быть успешно сохранена, происходит реальная проверка когда объект собирается изменить свое состояние, например, "заказанное" состояние изменяется на "приобретенное".

при создании экземпляр должен быть допустимым для сохранения, Что включает проверку уникальности. Он отличается от valid-for-ordering, где нужно проверять не только уникальность, но и, например, кредитоспособность клиента, а также доступность в магазине.

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

Edit: судя по другим ответам, правильное имя для такого "доменного сервиса" -спецификация. Я обновил свой ответ, чтобы отразить это, включая более подробный пример кода.

Я бы пошел с вариантом 3; создать сервис домен спецификации, которая инкапсулирует логику, которая выполняет проверку. Например, спецификация изначально вызывает репозиторий, но вы можете заменить его вызовом веб-службы в a последующие стадии. Наличие всей этой логики за абстрактной спецификацией будет держать общий дизайн более гибким.

чтобы запретить кому-либо редактировать имя без его проверки, сделайте спецификацию обязательным аспектом редактирования имени. Вы можете добиться этого, изменив API вашего объекта на что-то вроде этого:

public class User
{
    public string Name { get; private set; }

    public void SetName(string name, ISpecification<User, string> specification)
    {
        // Insert basic null validation here.

        if (!specification.IsSatisfiedBy(this, name))
        {
            // Throw some validation exception.
        }

        this.Name = name;
    }
}

public interface ISpecification<TType, TValue>
{
    bool IsSatisfiedBy(TType obj, TValue value);
}

public class UniqueUserNameSpecification : ISpecification<User, string>
{
    private IUserRepository repository;

    public UniqueUserNameSpecification(IUserRepository repository)
    {
        this.repository = repository;
    }

    public bool IsSatisfiedBy(User obj, string value)
    {
        if (value == obj.Name)
        {
            return true;
        }

        // Use this.repository for further validation of the name.
    }
}

ваш код вызова будет выглядеть примерно так:

var userRepository = IoC.Resolve<IUserRepository>();
var specification = new UniqueUserNameSpecification(userRepository);

user.SetName("John", specification);

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

Я бы использовал спецификация для инкапсуляции правило. Затем вы можете позвонить, когда свойство UserName обновляется (или из любого другого места, которое может понадобиться):

public class UniqueUserNameSpecification : ISpecification
{
  public bool IsSatisifiedBy(User user)
  {
     // Check if the username is unique here
  }
}

public class User
{
   string _Name;
   UniqueUserNameSpecification _UniqueUserNameSpecification;  // You decide how this is injected 

   public string Name
   {
      get { return _Name; }
      set
      {
        if (_UniqueUserNameSpecification.IsSatisifiedBy(this))
        {
           _Name = value;
        }
        else
        {
           // Execute your custom warning here
        }
      }
   }
}

это не будет иметь значения, если другой разработчик пытается изменить User.Name напрямую, потому что правило всегда будет выполняться.

здесь

Я не эксперт по DDD, но я задал себе те же вопросы, и это то, что я придумал: Логика проверки обычно должна входить в конструктор / фабрику и сеттеры. Таким образом, вы гарантируете, что у вас всегда есть действительные объекты домена. Но если проверка включает запросы к базе данных, которые влияют на производительность, эффективная реализация требует другого дизайна.

(1) Инъекционные Сущности: впрыскивая реальности могут быть технически трудны и также очень сложно управлять производительностью приложений из-за фрагментации логики базы данных. Казалось бы, простые операции теперь могут неожиданно повлиять на производительность. Это также делает невозможной оптимизацию объекта домена для операций с группами одного и того же типа сущностей, вы больше не можете написать один групповой запрос, и вместо этого у вас всегда есть отдельные запросы для каждой сущности.

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

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

в моей структуре CQRS каждый класс обработчика команд также содержит метод ValidateCommand, который затем вызывает соответствующую логику бизнеса/проверки в домене (в основном реализованную как методы сущности или статические методы сущности).

поэтому вызывающий абонент будет делать так:

if (cmdService.ValidateCommand(myCommand) == ValidationResult.OK)
{
    // Now we can assume there will be no business reason to reject
    // the command
    cmdService.ExecuteCommand(myCommand); // Async
}

каждый специализированный обработчик команд содержит логику оболочки, например:

public ValidationResult ValidateCommand(MakeCustomerGold command)
{
    var result = new ValidationResult();
    if (Customer.CanMakeGold(command.CustomerId))
    {
        // "OK" logic here
    } else {
        // "Not OK" logic here
    }
}

метод ExecuteCommand обработчика команд вызовет ValidateCommand () снова, так что даже если клиент не беспокоился, ничего не произойдет в домене, который не должен.

создайте метод, например, называемый IsUserNameValid () и сделайте его доступным отовсюду. Я бы сам поставил его в службу пользователей. Это не будет ограничивать вас при возникновении будущих изменений. Он сохраняет код проверки в одном месте (реализация), а другой код, который зависит от него, не должен будет меняться, если изменения проверки вы можете обнаружить, что вам нужно вызвать это из нескольких мест позже, например, пользовательский интерфейс для визуальной индикации без необходимости прибегать к исключению обращение. Уровень сервиса для корректных операций и репозиторий (кэш, БД и др.)) слой, чтобы убедиться, что сохраненные элементы действительны.

Мне нравится вариант 3. Простая реализация может выглядеть так:

public interface IUser
{
    string Name { get; }
    bool IsNew { get; }
}

public class User : IUser
{
    public string Name { get; private set; }
    public bool IsNew { get; private set; }
}

public class UserService : IUserService
{
    public void ValidateUser(IUser user)
    {
        var repository = RepositoryFactory.GetUserRepository(); // use IoC if needed

        if (user.IsNew && repository.UserExists(user.Name))
            throw new ValidationException("Username already exists");
    }
}

создать доменную службу

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

Если вы правильно спроектировали свои объекты, это не должно быть проблемой.