Репозитории DDD в приложении или доменном сервисе

29

В настоящее время я изучаю DDD, и у меня возникли некоторые вопросы относительно того, как управлять репозиториями с DDD.

На самом деле, я встретил две возможности:

Первый

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

Таким образом, в одном из методов службы приложения мы вызываем метод службы домена (проверка бизнес-правил), и, если условие выполняется, вызывается специальный репозиторий для сохранения / извлечения объекта из базы данных.

Простой способ сделать это может быть:

class ApplicationService{

  constructor(domainService, repository){
    this.domainService = domainService
    this.repository = repository
  }

  postAction(data){
    if(this.domainService.validateRules(data)){
      this.repository.persist(new Entity(data.name, data.surname))
    }
    // ...
  }

}

Второй

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

class ApplicationService{

  constructor(domainService){
    this.domainService = domainService
  }

  postAction(data){
    if(this.domainService.persist(data)){
      console.log('all is good')
    }
    // ...
  }

}

class DomainService{

  constructor(repository){
    this.repository = repository
  }

  persist(data){
    if(this.validateRules(data)){
      this.repository.save(new Entity(data.name))
    }
  }

  validateRules(data){
    // returns a rule matching
  }

}

Отныне я не могу отличить, какой из них лучший (если есть один лучший) или что они подразумевают в своем контексте.

Можете ли вы привести пример, где один может быть лучше другого и почему?

mfrachet
источник
«внедрить репозиторий и модель домена в сервис приложения». Что вы имеете в виду, вводя где-то «модель предметной области»? AFAICT в терминах модели домена DDD означает весь набор понятий из области и взаимодействий между ними, которые имеют отношение к приложению. Это абстрактная вещь, это не какой-то объект в памяти. Вы не можете ввести это.
Алексей

Ответы:

31

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

Назначение доменной службы

Доменные службы должны инкапсулировать понятия / логику домена - как таковой, метод доменной службы:

domainService.persist(data)

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

Обычно доменные службы полезны, когда у вас есть бизнес-правила / логика, которые требуют координации или работы с несколькими агрегатами. Если логика включает только один агрегат, он должен быть в методе на объектах этого агрегата.

Репозитории в Службах Приложений

Таким образом, в этом смысле, в вашем примере, я предпочитаю ваш первый вариант - но даже там есть возможности для улучшения, так как ваша доменная служба принимает необработанные данные из API - почему доменная служба должна знать о структуре data? Кроме того, данные, по-видимому, связаны только с одним агрегатом, поэтому использование службы домена для этого имеет ограниченную ценность - обычно я бы помещал проверку в конструктор сущностей. например

postAction(data){

  Entity entity = new Entity(data.name, data.surname);

  this.repository.persist(entity);

  // ...
}

и выбросить исключение, если оно недействительно. В зависимости от вашей прикладной среды может быть просто иметь согласованный механизм для перехвата исключения и сопоставления его с соответствующим ответом для типа API - например, для API REST, вернуть код состояния 400.

Репозитории в доменных службах

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

postAction(data){

  this.domainService.doSomeBusinessProcess(data.name, data.surname, data.otherAggregateId);

  // ...
}

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

doSomeBusinessProcess(name, surname, otherAggregateId) {

  OtherEntity otherEntity = this.otherEntityRepository.get(otherAggregateId);

  Entity entity = this.entityFactory.create(name, surname);

  int calculationResult = this.someCalculationMethod(entity, otherEntity);

  entity.applyCalculationResultWithBusinessMeaningfulName(calculationResult);

  this.entityRepository.add(entity);

}

Вывод

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

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

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

Крис Саймон
источник
Итак, если я придерживаюсь бизнес-правил (допускающих правила шаблонов спецификаций), если это касается только одной сущности, мне следует только проверить правильность этой сущности? Кажется странным вводить бизнес-правила, такие как контроль хорошего формата почты пользователя внутри сущности пользователя. Не так ли? Что касается глобального ответа, спасибо. Получилось, что нет «правила по умолчанию для применения», и это действительно зависит от наших сценариев использования. У меня есть работа, чтобы хорошо различать всю эту работу
mfrachet
2
Для пояснения, правила, принадлежащие объекту, являются только правилами, которые находятся в ведении этого объекта. Я согласен, контроль хорошего формата электронной почты пользователя не означает, что он принадлежит сущности User. Лично мне нравится помещать правила валидации в объект Value, который представляет адрес электронной почты. У пользователя будет свойство типа EmailAddress, а конструктор EmailAddress принимает строку и выдает исключение, если строка не соответствует требуемому формату. Затем вы можете повторно использовать EmailAddress ValueObject для других объектов, которым необходимо сохранить адрес электронной почты.
Крис Саймон
Хорошо, теперь я понимаю, почему использовать Value Object. Но это означает, что объект значения должен иметь свойство, которое является бизнес-правилом, управляющим форматом?
mfrachet
1
Объекты значения должны быть неизменными. Обычно это означает, что вы инициализируете и проверяете в конструкторе, и для любых свойств используйте публичный шаблон get / private set. Но вы можете использовать языковые конструкции для определения равенства, процесса ToString и т. Д., Например, kacper.gunia.me/ddd-building-blocks-in-php-value-object или github.com/spring-projects/spring-gemfire-examples/ blob / master /…
Крис Саймон
Спасибо @ChrisSimon, наконец, и ответ на реальную ситуацию DDD, которая включает в себя код, а не только теорию. Я потратил 5 дней на траление SO и Интернета для функционального примера создания и сохранения агрегата, и это самое ясное объяснение, которое я нашел.
e_i_pi
2

Есть проблема с принятым ответом:

Доменная модель не может зависеть от хранилища, и доменная служба является частью доменной модели -> доменная служба не должна зависеть от хранилища.

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

Исходя из вашего примера это может выглядеть так:

class ApplicationService{

  constructor(domainService, repository){
    this.domainService = domainService
    this.repositoryA = repositoryA
    this.repositoryB = repositoryB
    this.repositoryC = repositoryC
  }

  // any parsing and/or pre-business validation already happened in controller or whoever is a caller
  executeUserStory(data){
    const entityA = this.repositoryA.get(data.criterionForEntityA)
    const entityB = this.repositoryB.get(data.criterionForEntityB)

    if(this.domainService.validateSomeBusinessRules(entityA, entityB)){
      this.repositoryC.persist(new EntityC(entityA.name, entityB.surname))
    }
    // ...
  }
}

Итак, практическое правило: модель предметной области не зависит от внешних слоев

Приложение против доменного сервиса Из этой статьи :

  • Доменные сервисы очень детализированы, тогда как сервисы приложений являются фасадом, предназначенным для предоставления API.

  • Доменные сервисы содержат доменную логику, которая не может быть естественным образом помещена в сущность или объект-значение, тогда как сервисы приложений управляют выполнением доменной логики и сами не реализуют никакой доменной логики.

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

  • Сервисы приложений объявляют зависимости от инфраструктурных сервисов, необходимых для выполнения логики домена.

SMS
источник
1

Ни один из ваших шаблонов не годится, если ваши сервисы и объекты не заключают в себе какой-то согласованный набор ответственности.

Прежде всего, скажите, что представляет собой ваш доменный объект, и расскажите, что он может делать на языке домена. Если он может быть действительным или недействительным, почему бы не использовать его как свойство самого объекта домена?

Если, например, валидность объектов имеет смысл только с точки зрения другого объекта, то, возможно, у вас есть ответственность «правило проверки X для доменных объектов», которое может быть инкапсулировано в набор сервисов.

Требуется ли проверка объекта в соответствии с вашими бизнес-правилами? Возможно нет. Ответственность за «хранение объектов» обычно лежит в отдельном объекте хранилища.

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

Эта операция присуща доменному объекту? Затем сделайте его частью этого объекта, т.е.ExamQuestion.Answer(string answer)

Это соответствует какой-то другой части вашего домена? положи это тамBasket.Purchase(Order order)

Вы бы предпочли услуги ADM REST? Хорошо, тогда.

Controller.Post(json) 
{ 
    parse(json); 
    verify(parsedStruct); 
    save(parsedStruct); 
    return 400;
}
Ewan
источник