DDD: Составление агрегата для финансовой системы
Итак, контекст задачи - финансовая система. Пользователь имеет 4 счёта: Основной (Primary); Холдинговый (Holding); Гаранийный (Guarantee) и Страховой (Escrow). Об этом ему знать не обязательно. Для внешнего мира есть основной счёт и гарантийный депозит. Два оставшихся счёта нужны для обеспечения целостности системы, но это в контексте вопроса не важно.
Важно следующее. Имеется контекст FundsManagement с командой ShowMyFunds. Ответ содержит такое тело:
{
"balance": "1504.00",
"reserve": "0.00",
"status": "ACTIVE"
}
С точки зрения пользователя счёт всего один и мне не хотелось бы чтоб внешний мир знал что у меня творится под капотом. Поэтому ответ такой немногословный. При этом бэкофис знает что там множество счетов - он с ними работает.
Чтоб подвести к такому ответу нужно правильным образом организовать контекст, но тут у меня случился брейнлаг. Уже который день я мечусь между несколькими вариантами исполнения и никак не могу выбрать верное решение. Всё упирается в агрегат, который я до конца не понимаю. Не понимаю что даже поставить в его корень. К примеру, им может быть вот такой Account:
final class Account extends AggregateRoot
{
public Money $balance;
public Money $reserve;
public Status $status;
public function __construct(...) {...}
}
Дело в том, что этот счёт будет собираться из нескольких других счетов. Бизнесовая сущность не имеет свойства Money $reserve. Её приходится брать из другого счёта. Этот агрегат состоит как бы из двух счетов, а логика сборки уходит в инфраструктуру. Логика не простая. К примеру, статус и баланс берутся из PRIMARY счёта, а резерв из HOLDING. Правильно ли это? Я не уверен.
Другой вариант может выглядеть так
final class Account extends AggregateRoot
{
public Money $balance;
public Type $type;
public Status $status;
public function __construct(...) {...}
}
В таком случае мне нужно доставать сразу несколько агрегатов по типу и собирать ответ на уровне приложения. К примеру, в классе ShowMyFundsCommandResult. Правильно ли это? Будто бы да, но тогда контекст лишается возможности перевода между счетами т.к. по канонам DDD в рамках одного действия допустимо изменять только один аггрегат. В случае перевода меняются два, что этот принцип нарушает.
Третий вариант на данный момент реализован в коде и в качестве корня имеет сущность самого пользователя. Вроде бы вся логика упакована как нужно, но меня терзают смутные сомнения. Нужен кто-то кто хорошо понимает что такое агрегат и кто может дать мне совет.
Заранее благодарен.
final class User extends AggregateRoot
{
public array $accounts;
public function __construct(Identifier $id, array $accounts)
{
parent::__construct($id);
foreach ($accounts as $account) $this->accounts[
$account->type->name] = $account;
}
private function getAccount(Type $type): Account
{
$resultOrNull = $this->accounts[$type->name] ?? null;
if (!$resultOrNull) throw new ActionProhibitedError();
return $resultOrNull;
}
public function getActualBalance(): Money
{
return $this->getAccount(Type::PRIMARY)->balance;
}
public function getReserveBalance(): Money
{
return $this->getAccount(Type::HOLDING)->balance;
}
public function getActualStatus(): Status
{
return $this->getAccount(Type::PRIMARY)->status;
}
}
Сущность аккаунта
final class Account extends AbstractEntity
{
public Money $balance;
public Type $type;
public Status $status;
public \DateTime $updatedAt;
public function __construct(...) {...}
}
И уровень приложения
readonly class ShowMyFundsCommandResult extends AbstractCommandResult
{
public string $balance;
public string $reserve;
public string $status;
public function __construct(User $user)
{
$this->balance = $user->getActualBalance()->formatToMajorStyle();
$this->reserve = $user->getReserveBalance()->formatToMajorStyle();
$this->status = $user->getActualStatus()->name;
}
}