Comments 3
Как забавно это читать, после того, как уже переболел всем этим DDD. У меня складывается устойчивое впечатление, что DDD придумали программисты-аутисты, чтобы как можно сильнее запудрить нормальным программистам мозги. Вы понимаете, что это не работает на практике? Есть т.н. трилемма DDD. И у вас в статье в решении каждой из проблем этой трилеммы нужно вкорячивать костыль?
Какой смысл тогда всего этого - если может быть ли бо XDD, DXD или DDX, а чистое DDD невозможно или нецелесообразно физически? В предыдущем предложении, если что, латинские буквы D взяты как условность, а не в качестве аббревиатуры DDD.
Когда мы решаем работать с моделями через паттерн Transaction Script (т.н. ваш LoyaltyProgramService::сhangeCurrency()). Мы можем в принципе обозвать метод как changeCurrencyTransaction и в CONTRIBUTING.md проекта прописать условность, что все методы сервисов имеющих в названии суффикс Transaction - должны содержать в себе транзакционную обработку всего что попадает в этот метод. И это даже можно контролировать в пайланах CI/CD.
Когда мы имеем лаконичный, модный, трушный, чистый ДэДэДэшный интерфейс LoyaltyProgramRepositoryInterface::save(LoyaltyProgram $loyaltyProgram): void, то где та метаинформация для того, кто будет реализовывать этот интерфейс, что вот смотри чувак, у тебя в LoyaltyProgram есть еще куча связанных сущностей, которые тебе нужно заперсистить в хранилище и все это в общую транзакцию обернуть?
У вас либо бизнес-логика протекает в Applayer, любое инфраструктурная в доменный слой. Либо у вас неполноценные и не трушные модели. Но тогда это Колосс на глиняных ногах.
С точки зрения удобства в коде нам конечно было бы лучше всего сделать так:
// Инвариант: нельзя получить скидку по неактивной карте
return null;
А потом приходит менеджер и говорит "У меня в админке для неактивных карт не показывается какая там была скидка, сделайте чтобы показывалось".
Минусы такого решения очевидны:
Существует возможность изменить валюту накопления, забыв сбросить уровни лояльности.
Такая возможность существует даже с логикой в сущностях.
Пример есть прямо у вас в статье, только наоборот. Вы в LoyaltyProgram::changeCurrency() пропустили установку newCurrency.
За транзакцию отвечает сервис, а значит, нужно следить и за тем, чтобы никто не забывал оборачивать весь код с бизнес-логикой в транзакции по всей кодовой базе.
Это аналогично утверждению "нужно следить за тем, чтобы никто не забывал проверять инварианты / делать валидацию входных данных / писать логику в правильной сущности".
Никакой проблемы в этом нет. Ваш подход содержит гораздо больше вариантов за чем надо следить, и потому требует гораздо больше усилий.
$this->levels->resetRequiredAmounts(); //внутри foreach по уровням
Сама необходимость такого коммента говорит о том, что ваш код менее понятный, чем был в сервисе.
Теперь изменение валюты невозможно без обнуления настроек уровней
Да ну как это невозможно.
Вы сказали, что с сервисом можно написать новый метод, в котором не будет обнуления настроек уровней.
C логикой в сущностях будет точно так же.
class LoyaltyProgram
{
public function changeCurrency(LoyaltyCurrency $newCurrency): void
{
...
}
public function changeCurrencyNew(LoyaltyCurrency $newCurrency): void
{
if ($newCurrency === $this->currency) {
return;
}
$this->currency = $newCurrency;
// не сбрасываем уровни
}
}
а сохранение всего агрегата происходит атомарно
Ну то есть в проекте появилось больше магии. К тому же транзакция теперь запускается всегда, а не только когда нужно, что влияет на производительность.
и мы при написании кода об этом можем больше не задумываться
Зато должны задумываться о куче других вещей. Например искать где находится foreach по уровням.
А также: писать доменный сервис или не писать, как не загружать историю всех событий, как изменить свойства вложенного объекта LoyaltyCard, и т.д.
не может быть ни микросекунды времени, когда ваш агрегат содержит неправильные с точки зрения бизнес-логики данные
в приведенном выше примере объект класса LoyaltyProgram всегда содержит в себе консистентные данные
Это ложь. В середине вызова changeCurrency, после установки newCurrency (которой у вас нет) и до вызова resetRequiredAmounts объект класса LoyaltyProgram содержит неконсистентные данные.
А если оценивать только после завершения метода с логикой, то и с сервисом надо так же делать.
Когда вся логика сосредоточена в агрегатах, сделать это сложнее.
Да нисколько не сложнее. Точно так же берем и пишем новый метод с любой логикой.
При сохранении агрегата в репозитории мы обеспечим атомарное сохранение и того и другого.
$changesObserver = DoctrineEntityChangesObserver::instance();
foreach ($changesObserver->getNewMaterials() as $material)
А чего это у вас репозиторий сохраняет данные, не относящиеся к агрегату? Вы же только что доказывали, что они должны быть частью агрегата. Кроме того, теперь бизнес-логика действия находится не только в сущности, а еще и в неком MaterialAddedSubscriber.
Глобальные переменные, синглтоны. И на это вы предлагаете заменять простой тестируемый сервис с зависимостями в конструкторе?
DomainEventPublisher::instance()->publish(new LoyaltyCardRequested())
$event->collection->initCard($card);
Угу, добавим еще больше магии. Убрали event subscriber, и всё перестало работать. Где тут простота поддержки, для которой мы хотели использовать DDD?
и вы возлагаете на слой Application ответственность вытащить карту из репозитория и обернуть в транзакцию сохранение ПЛ и карты
Ну то есть фактически пишете сервис.
как проектировать доменный агрегат, чтобы он не стал безразмерным
К сожалению, тут нет четких универсальных рецептов.
И здесь нет однозначно правильных вариантов.
Ну то есть ответа у вас нет.
определение границ агрегата
Скажите, а DDD композиция существует? Как у неё проверить границы?))
А если серьёзно, берем S из одной известной абревиатуры и получается что для проектирования границ модуля бизнес логики (что у вас называется DDD агрегатом как я понял) нужно выделить так называемых акторов (группу лиц) заинтересованных в данном функционале и скорее всего которые будут вносить доработки по нему, получается что функциональность для каждого актора (группы лиц) и есть границы модулей (агрегатов DDD?)
Разбираемся с DDD: как проектировать доменный агрегат, чтобы он не стал безразмерным