Подтвердить что ты не робот

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

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

Итак, у нас есть объект 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);
}

Параметры:

  • Вложение репозитория для объекта
  • Вставить репозиторий в factory
  • Создать операцию в службе домена
  • ???

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

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, чтобы скрыть зависимость между User и IUserRepository через объект Specification.

2. Инъекционный репозиторий factory

Я могу поместить эту логику проверки в 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.???

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

Спасибо!

4b9b3361

Ответ 1

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

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

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

Пример

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.

Эта статья статьи в шаблоне спецификации с приложением в .Net может вас заинтересовать.

Ответ 2

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

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

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

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

Ответ 3

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

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

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

Ответ 4

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

Узнайте больше здесь

Ответ 5

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

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

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

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

Ответ 6

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

Таким образом, вызывающий абонент будет делать следующее:

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(), поэтому даже если клиент не беспокоился, в домене не должно быть ничего.

Ответ 7

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

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

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

Ответ 8

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

Ответ 9

Мне нравится вариант 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");
    }
}