В контроллере тоже кстати может понадобиться проверять, на пример если у вас не API а сервер-сайд рендеринг, то на одно исключение может происходить редирект на другую страницу, а на другое рендеринг какого-то определенного шаблона.
RuntimeException можно поймать в любом коде. Обработка результата isFundsSufficient может происходить только в вызывающем коде, больше этот результат нигде не нужен. Не в контроллере же проверять.
Может быть например в подписчике на событие который вызывает метод не напрямую через агрегат, а через службу приложеиня или шину команд. И ему нужно понять что именно произошло а не абстрактный RuntimeException.
Иногда может касаться и не только внутреннего состояния. Как это связано со спецификацией, я не понял.
Описываем внутрее состояние с помощью спецификации и используем ее внутри агрегата при записи и для рид модели при чтении.
SRP не заставляет разбивать связанную логику на несвязанные части. Есть принцип Low Coupling, и в вашем варианте вызывающий и вызываемый код получаются highly coupled, потому что содержат детали реализации одного уровня.
Как раз наоборот. Есть две операции: списание данных счета и уведомление пользователя о недостатке средств. О том как списать средства знает агрегат Счет. О том как уведомить пользователя знает Система Уведомлений. Если требования к механизму уведомления изменяться, например у пользователя поятвиться новый канал связи, телефон, или мессенджер, либо пользователь захочет включить или выключить какой-то из каналов связи, то изменения затронут только Систему Уведомления, но никак не затронут агрегат Счет. Если измениться логика списания денег со счета, то измениться только агрегат Счет, никак не затрагивая систему уведомлений. У этих программных модулей разная зона ответственности и они между собой слабо связаны.
В моем примере кода агрегат контролирует свои инварианты. Функция валидации вызывается и из внешнего кода, и из performAction в сущности.
Да но у вас может возникнуть проблема если обработка ошибки будет посходить не непосредственно в вызывающем коде, а где-нибудь выше. Механизм исключений как раз предназначен для того чтобы эту проблему решить.
Какая разница, проверки для кнопки все равно должны быть те же самые.
Согласен, в этом случае, если проверка касается только внутреннего состояния объекта, чтобы не дублировать код, можно применить паттерн Спецификация.
Проверка в одном месте, обработка ее результата в другом.
Ну да, у нас же SRP, агрегат отвечает за целостность своего состояния, все остальное не его ответственность.
Для него в данном случае событие это факт совершения бизнес-действия, когда совершать его нельзя, и он хочет отправлять уведомление об этом событии
Немного не так, произошла попытка выполнить бизнес действие, которая закончилась неудачей. Агрегат об этом проинформировал, а вызывающий код принял решение как на это отреагировать.
Поэтому я и говорю, что вызовы проверки и отправки должны быть вне агрегата
Отправки да, проверки не, иначе агрегат не будет контролировать свои инварианты.
потому что бизнес часто хочет дизэйблить кнопку в интерфейсе, если действие нельзя выполнить.
Это уже операция чтения а не записи, а чтение это про read model, богатая модель используется только для операций записи.
Это как раз неявное поведение, про которое я говорил. Мы делаем исключение, которое на самом деле не исключение а событие,
Событие публикуется когда изменяется состояние агрегата, в данном случае агрегат не может изменить состояние и не может эту ситуацию обработать, поэтому бросает исключение.
Если для этой ситуации надо отправлять уведомление, в отличие от других ошибок, то надо делать это явно с пробросом eventBus или emailClient, и без исключений, а с обычным if
Отправка email не является ответственностью агрегата, поскольку этот сайд эффект никак не связан с его состоянием. Агрегат в рамках бизнес метода может изменять только свое состояние и не должен, за очень редкими исключениями напрямую посылать команды на изменение состояния вовне.
На самом деле слой адаптеров тоже валидирует. Просто он делает совершенно другие валидации: он проверяет что входящий DTO соответствует требованиям API (форматы значений, обязательные поля, etc.).
Именно про такую валидацию я и говорю, я не предлагаю дублировать валидацию бизнес правил в слое презентации. И как правило UI требует все ошибки скопом именно после такой валидации, а не после валидации бизнес правил.
Как правило так и происходит, но иногда бывают ситуации когда вызывающий код реагирует на конкретную ошибку, например на InssuficientFundsExceptionотправит сообщение в шину, которое перехватит сервис нотификации и пришлет пользователю определенное сообщение
Кроме этого, если мы например используем Rest API разные исключение могут мапится на разные коды http. Можно конечно на каждый такой код сделать свой тип исключений, но на мой взгляд это будет жёсткая привязка домена к конкретному типу API.
Проблема в том, что со временем выясняется, что большинство UI рано или поздно захочет все ошибки вместо одной, иначе юзабилити слишком страдает
Мне кажется, что эту проблему нужно решать на уровне слоя презентации а не домена. Как правило те ошибки, которые UI требует сразу группой, касаются формата данных и проверок обязательных полей, и такую валидацию можно провести а слое презентации ещё до трансформации входных данных в типы и передачи их в слой домена. Ситуация когда UI хочет знать одновременно, например что на счету недостаточно денег и он также заблокирован на мой взгляд возникают достаточно редко.
Ясно. В чистой и гексагональной архитектурах это называется слой адаптеров, в луковой презентационным слоем. Я предполагал, что речь о нём, но никогда не слышал, чтобы его называли "слоем UI на бэке".
Да, это слой презентации, в некоторых описаниях многоуровневой архитектуры его называют слоем UI.
А кто тут является клиентом?
Любой класс, который непосредственно либо опосредованно вызывает метод агрегата, и может обработать исключение брошенное агрегатом. И с моей точки зрения, такому клиенту будет гораздо удобнее получать исключения разных типов, которые являются частью публичного интерфейса агрегата, чем один ValidationException, в котором нужно будет дополнительно анализировать массив ошибок используя сторонние библиотеки.
Добавление сюда любых подробностей (напр. какая именно валидация провалена) - это уже автоматом удовлетворение нужд UI, а не домена
Я смотрю на это немного под другим углом. Если в процессе выполнения бизнес операции агрегат столкнулся с ситуацией когда он эту операцию выполнить не может, то в зоне ответственности агрегата это исключительная ситуация, на которую он не знает как реагировать и поэтому он бросает исключение передавая ответственность вызывающему коду. С моей точки зрения лучший способ сообщить максимально подробно что именно произошло это вернуть в ответе специальный тип, в данном случае исключение, из которого вызывающий код сможет получить всю информацию которая его интересует, чтобы принять решение, как эту исключительную ситуацию обработать. Если вместо специального типа мы будем бросать ValidationException, то во первых интерфейс агрегата будет зависеть от сторонней библиотеки, а во вторых, это затруднит вызывающему коду понимание что именно произошло. Поэтому группировку ошибок я бы использовал только в ситуации если есть очень жесткое требование со стороны UI и его никак нельзя обойти, но по умолчанию бросал бы типизированные исключения по отдельности, для каждой исключительной ситуации.
Да, валидация входных данных, на предмет правильности формата, наличия всех необходимых данных и конвертация этих данных в нужные типы должна происходить вне агрегата и слоя домена, в driver адаптере. А вот валидация бизнес правил должна осуществляться внутри доменного объекта.
Не всегда ошибка привязана к свойству сущности. Да и не всегда метод в сущности тригерится как реакция на отправку формы, поэтому валидация бизнес правил не должна быть привязана к полю на форме. Провалидировать NotBlank в сущности вообще невозможно, например в языках с типизацией и nullsafe. Как вы передадите blank значение в метод doSomething(int $value):void
С другой стороны в варианте без группировки, он бы тоже обрабатывал только первое исключение, так что возможно это и не является проблемой и итерация по вложенной коллекции не нужна. Но все равно идея группировки исключений в агрегате, немного попахивает влиянием требований слоя UI на бизнес логику, чего в теории быть не должно, хотя на практике конечно этого избежать порой трудно.
Хотя нет, все равно клиенту придется итерировать по коллекции. Возьмем пример который я привел в одном их предыдущих комментариев:
<?php
final class Account
{
private $blocked = false;
private Money $balance;
public function withdraw(Money $amount): void
{
if ($this->balance->isLessThan($amount)) {
throw new InssuficientFundsException();
}
if ($this->blocked) {
throw new AccountBlockedException();
}
$this->balance = $this->balance->subtract($amount);
}
}
Допустим у нас есть подписчик который обрабатывает AccountBlockedException
Если мы будем возвращать коллекцию исключений, то этому подписчику нужно будет ловить все исключения которые бросает метод withdraw проверять не является ли корневое исключение AccountBlockedException и если нет, то проверять нет ли этого исключения во вложенной коллекции. На мой взгляд это сильно усложняет код обработки ошибок.
Это слой который отвечает например за преобразование http запроса в CQRS команду или запрос, передачу его в слой application и преобразование ответа из слоя application в http ответ.
Для этого есть стандартный подход: в ответе API в поле "error"
Хм, действительно, есть такой подход, просто я никогда не рассматривал его использование на уровне интерфейса агрегата, но почему нет, бросаем первое исключение, а внутрь него кладем коллекцию последующих. Спасибо за подсказку.
Это будет медленно в UI (надо дёрнуть кучу API перед отправкой основного запроса).
Вы меня немного не правильно поняли, я имел ввиду слой UI бэкенд приложения, а не фронтенд приложение.
Просто найдите или напишите сами библиотечку для валидаций, которая будет уметь возвращать список ошибок.
Но ведь клиентом метода агрегата может быть не только слой UI, а например какой -то сабскрайбер, который каким-то образом может реагировать на ошибку которую возвращает метод агрегата. И код этого клиента будет намного проще если агрегат будет возвращать одну ошибку, а не целый набор. Например в случае использования исключений это будет простой try catch без необходимости итерировать по коллекции вложенных ошибок.
Но а целом я с вами согласен, метод громоздкий, и к счастью на практике его применять не приходилось, обычно ошибки, которые нужно увидеть группой, касаются простых проверок типа формата входных данных или их наличия и их вполне можно сделать библиотекой валидации, но в слое UI, а не в агрегате.
если валидации провалились, то вернуть группу соответствующих доменных ошибок
Вот этот момент меня всегда немного смущал. Если мы будем делать проверки бизнес правил последовательно, то код в слое домена будет проще, но ошибку он будет возвращать одну. Кроме этого, мне кажется, что то каким образом ошибки будут отображаться это скорее требование слоя UI. При простых валидациях, типа формат данных, обязательное не обязательное поле эти проверки легко можно продублировать в слое UI, но если такая валидация требует каких-то проверок с связанных с внутренней бизнес логикой агрегата, то мне кажется что лучше сделать в агрегате публичный метод который проверяет бизнес правило и возвращает boolean, который может дёрнуть UI, чем усложнять логику агрегата группировкой ошибок.
Я вам уже показал преимущества которое я вижу в использовании богатой модели в слое домена, на примере операции списания средств со счета, вас этот пример не убедил, вы преимуществ в объединении поведения и состояния не видите. Реализация слоя UI с моей точки зрения ничего принципиально не изменит, поэтому не вижу смысла тратить на это время. Если для вас подход с сервисами и анемичными моделями работает - отлично, я лично вижу в этом подходе ряд недостатков, которые озвучил. Больше мне добавить нечего.
Ок, вы не понимаете преимущества ограниченных контекстов, инкапсуляции, принципа inversion of control, CQRS и похоже мне не удасться до вас эту информацию донести. Если вас устраивает подход который вы описали в статье, и он позволят вам создавать качественные, поддерживаемые приложения, то это очень хорошо и я могу только за вас порадоваться. Но я бы на вашем месте не стал бы так категорично объявлять этот подход лучше тех подходов которые вы не понимаете.
Когда в сущности 3000 строк и пара сотен методов, не знать про нужный метод или пропустить его тоже очень вероятно.
Именно для этого в DDD и придуманы ограниченные контексты. Кроме этого, даже в рамках одного контеста, никто не запрещает разбить сущность на велью объекты и делегировать им часть доменной логики.
Про сервисы не надо знать, нажимаем "Find usages" в IDE и получаем все места, где используется сущность
Ну да, это сделать гораздо проще, чем проанализировать код объекта который модифицируешь.
Если кто-то напрямую модифицирует баланс, значит он реализовывает новые бизнес-требования, и существующие проверки ему не нужны. Если нужны, он их вызовет из своего кода.
Если не забудет :).
Вот именно потому что часть данных находится за пределами агрегата. Если они нужны сущности для изменения состояния, их надо передавать в аргументах метода. Загружать данные это не ответственность сущности.
Она их и не загружает, а всего лишь объявляет какие данные ей нужны через интерфейс, но ничего не знает откуда и как эти данные загружаются, этим занимается слой инфраструктуры, который реализует этот интерфейс.
Неважно какой это слой, эта информация связана с бизнес-логикой, поэтому ее тоже надо рассматривать, что я и предложил.
Важно, ответственность слоя UI ограничивается преобразованием данных в понятный для слоя приложения формат, и передача их в слой приложения и обратно. Никакой бизнес логики там быть не должно. Вся бизнес логика должна находится в слое домена и реализована либо в процедурном стиле (Amemic model) либо при помощи ООП (Rich model). Но в большинстве приложений, как правило, нету четкого разделения слоев и парадигм, обычно там бешеная смесь в которой трудно что либо понять.
Да ну как это не могу-то) public function withdrawWithoutCheck() {
Только никто в здравом уме не станет такой метод писать, при наличии в объекте метода с проверкой. А вот если методы изменяющие состояние этого объекта будут разбросаны по разным сервисам, то вероятность что кто-то в каком то сервисе напрямую модифицирует баланс без всяких проверок, потому что просто не знает что есть сервис с такой проверкой, гораздо выше.
Вот я как раз и объясняю, что если вам понадобилось пробрасывать в сущность внешние зависимости, значит в ней находится больше одной ответственности.
Это почему же? Есть операция списание средств со счета. Данная операция изменяет только состояние агрегата счет. Перед изменением этого состояния объект счет должен выполнить ряд проверок чтобы убедиться что такое изменение возможно. Часть данных для этих проверок находится за пределами агрегата, поэтому он их запрашивает из внешнего мира (outside). Если алгоритм списания измениться то это изменение произойдет только внутри агрегата Счет, не затрагивая другие агрегаты. У агрегата счет одна ответственность, контроль всех операций которые изменяют его состояние.
Работы с UI там вообще нет, есть только JSON-ответ со списком ошибок валидации.
Вы похоже не понимаете что такое слой UI в бэкенд приложении и за что этот слой отвечает.
В контроллере тоже кстати может понадобиться проверять, на пример если у вас не API а сервер-сайд рендеринг, то на одно исключение может происходить редирект на другую страницу, а на другое рендеринг какого-то определенного шаблона.
Может быть например в подписчике на событие который вызывает метод не напрямую через агрегат, а через службу приложеиня или шину команд. И ему нужно понять что именно произошло а не абстрактный RuntimeException.
Описываем внутрее состояние с помощью спецификации и используем ее внутри агрегата при записи и для рид модели при чтении.
Как раз наоборот. Есть две операции: списание данных счета и уведомление пользователя о недостатке средств. О том как списать средства знает агрегат Счет. О том как уведомить пользователя знает Система Уведомлений. Если требования к механизму уведомления изменяться, например у пользователя поятвиться новый канал связи, телефон, или мессенджер, либо пользователь захочет включить или выключить какой-то из каналов связи, то изменения затронут только Систему Уведомления, но никак не затронут агрегат Счет. Если измениться логика списания денег со счета, то измениться только агрегат Счет, никак не затрагивая систему уведомлений. У этих программных модулей разная зона ответственности и они между собой слабо связаны.
Да но у вас может возникнуть проблема если обработка ошибки будет посходить не непосредственно в вызывающем коде, а где-нибудь выше. Механизм исключений как раз предназначен для того чтобы эту проблему решить.
Согласен, в этом случае, если проверка касается только внутреннего состояния объекта, чтобы не дублировать код, можно применить паттерн Спецификация.
Ну да, у нас же SRP, агрегат отвечает за целостность своего состояния, все остальное не его ответственность.
Немного не так, произошла попытка выполнить бизнес действие, которая закончилась неудачей. Агрегат об этом проинформировал, а вызывающий код принял решение как на это отреагировать.
Отправки да, проверки не, иначе агрегат не будет контролировать свои инварианты.
Это уже операция чтения а не записи, а чтение это про read model, богатая модель используется только для операций записи.
Событие публикуется когда изменяется состояние агрегата, в данном случае агрегат не может изменить состояние и не может эту ситуацию обработать, поэтому бросает исключение.
Отправка email не является ответственностью агрегата, поскольку этот сайд эффект никак не связан с его состоянием. Агрегат в рамках бизнес метода может изменять только свое состояние и не должен, за очень редкими исключениями напрямую посылать команды на изменение состояния вовне.
Именно про такую валидацию я и говорю, я не предлагаю дублировать валидацию бизнес правил в слое презентации. И как правило UI требует все ошибки скопом именно после такой валидации, а не после валидации бизнес правил.
Как правило так и происходит, но иногда бывают ситуации когда вызывающий код реагирует на конкретную ошибку, например на
InssuficientFundsException
отправит сообщение в шину, которое перехватит сервис нотификации и пришлет пользователю определенное сообщениеКроме этого, если мы например используем Rest API разные исключение могут мапится на разные коды http. Можно конечно на каждый такой код сделать свой тип исключений, но на мой взгляд это будет жёсткая привязка домена к конкретному типу API.
Мне кажется, что эту проблему нужно решать на уровне слоя презентации а не домена. Как правило те ошибки, которые UI требует сразу группой, касаются формата данных и проверок обязательных полей, и такую валидацию можно провести а слое презентации ещё до трансформации входных данных в типы и передачи их в слой домена. Ситуация когда UI хочет знать одновременно, например что на счету недостаточно денег и он также заблокирован на мой взгляд возникают достаточно редко.
Да, это слой презентации, в некоторых описаниях многоуровневой архитектуры его называют слоем UI.
Любой класс, который непосредственно либо опосредованно вызывает метод агрегата, и может обработать исключение брошенное агрегатом. И с моей точки зрения, такому клиенту будет гораздо удобнее получать исключения разных типов, которые являются частью публичного интерфейса агрегата, чем один ValidationException, в котором нужно будет дополнительно анализировать массив ошибок используя сторонние библиотеки.
Я смотрю на это немного под другим углом. Если в процессе выполнения бизнес операции агрегат столкнулся с ситуацией когда он эту операцию выполнить не может, то в зоне ответственности агрегата это исключительная ситуация, на которую он не знает как реагировать и поэтому он бросает исключение передавая ответственность вызывающему коду. С моей точки зрения лучший способ сообщить максимально подробно что именно произошло это вернуть в ответе специальный тип, в данном случае исключение, из которого вызывающий код сможет получить всю информацию которая его интересует, чтобы принять решение, как эту исключительную ситуацию обработать. Если вместо специального типа мы будем бросать ValidationException, то во первых интерфейс агрегата будет зависеть от сторонней библиотеки, а во вторых, это затруднит вызывающему коду понимание что именно произошло. Поэтому группировку ошибок я бы использовал только в ситуации если есть очень жесткое требование со стороны UI и его никак нельзя обойти, но по умолчанию бросал бы типизированные исключения по отдельности, для каждой исключительной ситуации.
Да, валидация входных данных, на предмет правильности формата, наличия всех необходимых данных и конвертация этих данных в нужные типы должна происходить вне агрегата и слоя домена, в driver адаптере. А вот валидация бизнес правил должна осуществляться внутри доменного объекта.
Не всегда ошибка привязана к свойству сущности. Да и не всегда метод в сущности тригерится как реакция на отправку формы, поэтому валидация бизнес правил не должна быть привязана к полю на форме. Провалидировать NotBlank в сущности вообще невозможно, например в языках с типизацией и nullsafe. Как вы передадите blank значение в метод
doSomething(int $value):void
С другой стороны в варианте без группировки, он бы тоже обрабатывал только первое исключение, так что возможно это и не является проблемой и итерация по вложенной коллекции не нужна. Но все равно идея группировки исключений в агрегате, немного попахивает влиянием требований слоя UI на бизнес логику, чего в теории быть не должно, хотя на практике конечно этого избежать порой трудно.
Хотя нет, все равно клиенту придется итерировать по коллекции. Возьмем пример который я привел в одном их предыдущих комментариев:
Допустим у нас есть подписчик который обрабатывает
AccountBlockedException
Если мы будем возвращать коллекцию исключений, то этому подписчику нужно будет ловить все исключения которые бросает метод
withdraw
проверять не является ли корневое исключениеAccountBlockedException
и если нет, то проверять нет ли этого исключения во вложенной коллекции. На мой взгляд это сильно усложняет код обработки ошибок.Это слой который отвечает например за преобразование http запроса в CQRS команду или запрос, передачу его в слой application и преобразование ответа из слоя application в http ответ.
Хм, действительно, есть такой подход, просто я никогда не рассматривал его использование на уровне интерфейса агрегата, но почему нет, бросаем первое исключение, а внутрь него кладем коллекцию последующих. Спасибо за подсказку.
Вы меня немного не правильно поняли, я имел ввиду слой UI бэкенд приложения, а не фронтенд приложение.
Но ведь клиентом метода агрегата может быть не только слой UI, а например какой -то сабскрайбер, который каким-то образом может реагировать на ошибку которую возвращает метод агрегата. И код этого клиента будет намного проще если агрегат будет возвращать одну ошибку, а не целый набор. Например в случае использования исключений это будет простой try catch без необходимости итерировать по коллекции вложенных ошибок.
Но а целом я с вами согласен, метод громоздкий, и к счастью на практике его применять не приходилось, обычно ошибки, которые нужно увидеть группой, касаются простых проверок типа формата входных данных или их наличия и их вполне можно сделать библиотекой валидации, но в слое UI, а не в агрегате.
Вот этот момент меня всегда немного смущал. Если мы будем делать проверки бизнес правил последовательно, то код в слое домена будет проще, но ошибку он будет возвращать одну. Кроме этого, мне кажется, что то каким образом ошибки будут отображаться это скорее требование слоя UI. При простых валидациях, типа формат данных, обязательное не обязательное поле эти проверки легко можно продублировать в слое UI, но если такая валидация требует каких-то проверок с связанных с внутренней бизнес логикой агрегата, то мне кажется что лучше сделать в агрегате публичный метод который проверяет бизнес правило и возвращает boolean, который может дёрнуть UI, чем усложнять логику агрегата группировкой ошибок.
Я вам уже показал преимущества которое я вижу в использовании богатой модели в слое домена, на примере операции списания средств со счета, вас этот пример не убедил, вы преимуществ в объединении поведения и состояния не видите. Реализация слоя UI с моей точки зрения ничего принципиально не изменит, поэтому не вижу смысла тратить на это время. Если для вас подход с сервисами и анемичными моделями работает - отлично, я лично вижу в этом подходе ряд недостатков, которые озвучил. Больше мне добавить нечего.
Ок, вы не понимаете преимущества ограниченных контекстов, инкапсуляции, принципа inversion of control, CQRS и похоже мне не удасться до вас эту информацию донести. Если вас устраивает подход который вы описали в статье, и он позволят вам создавать качественные, поддерживаемые приложения, то это очень хорошо и я могу только за вас порадоваться. Но я бы на вашем месте не стал бы так категорично объявлять этот подход лучше тех подходов которые вы не понимаете.
Именно для этого в DDD и придуманы ограниченные контексты. Кроме этого, даже в рамках одного контеста, никто не запрещает разбить сущность на велью объекты и делегировать им часть доменной логики.
Ну да, это сделать гораздо проще, чем проанализировать код объекта который модифицируешь.
Если не забудет :).
Она их и не загружает, а всего лишь объявляет какие данные ей нужны через интерфейс, но ничего не знает откуда и как эти данные загружаются, этим занимается слой инфраструктуры, который реализует этот интерфейс.
Важно, ответственность слоя UI ограничивается преобразованием данных в понятный для слоя приложения формат, и передача их в слой приложения и обратно. Никакой бизнес логики там быть не должно. Вся бизнес логика должна находится в слое домена и реализована либо в процедурном стиле (Amemic model) либо при помощи ООП (Rich model). Но в большинстве приложений, как правило, нету четкого разделения слоев и парадигм, обычно там бешеная смесь в которой трудно что либо понять.
Только никто в здравом уме не станет такой метод писать, при наличии в объекте метода с проверкой. А вот если методы изменяющие состояние этого объекта будут разбросаны по разным сервисам, то вероятность что кто-то в каком то сервисе напрямую модифицирует баланс без всяких проверок, потому что просто не знает что есть сервис с такой проверкой, гораздо выше.
Это почему же? Есть операция списание средств со счета. Данная операция изменяет только состояние агрегата счет. Перед изменением этого состояния объект счет должен выполнить ряд проверок чтобы убедиться что такое изменение возможно. Часть данных для этих проверок находится за пределами агрегата, поэтому он их запрашивает из внешнего мира (outside). Если алгоритм списания измениться то это изменение произойдет только внутри агрегата Счет, не затрагивая другие агрегаты. У агрегата счет одна ответственность, контроль всех операций которые изменяют его состояние.
Вы похоже не понимаете что такое слой UI в бэкенд приложении и за что этот слой отвечает.