All streams
Search
Write a publication
Pull to refresh
8
0
Mike Shapovalov @mike_shapovalov

Software Engineer

Send message

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

Целью примера было показать как абстрагировать получение внешних данных для принятия бизнес-решения. Агрегат не может инициировать изменение состояния внешних сервисов напрямую через outside. Одной из целей outside как раз и является не позволить агрегату изменять состояние внешних сервисов. Резервация нужной суммы в банке должна происходить только после сохранения состояния агрегата в хранилище. Как правило это происходит после того как состояние агрегата записалось в хранилище и было опубликовано соответствующее доменное событие.

Это не совсем так. Гексагональная/луковая/чистая архитектуры требуют конвертации данных внешнего мира в формат удобный домену. Так что никаких изменений интерфейса не будет, пока в них либо не начнёт нуждаться бизнес-логика домена либо изменения внешнего мира сделают невозможным представление данных в таком виде. В обоих случаях изменения внутренней логики агрегата вполне нормальны и ожидаемы.

Смотря как подходить к изоляции. Мы подходим исходя из принципа Домен/Ограниченный Контекст/Агрегат. В этом случае на уровне слоя инфраструктуры мы объявляем интерфейс который являться anti-corruption layer по отношению к внешним сервисам (в данном примере к сервисам банков). Этот интерфейс описывает все методы которые требует данный ограниченный контекст и включает себя как методы для получения состояния так и для изменения. Интерфейс описывает требования всего ограниченного контекста и может использоваться несколькими агрегатами этого ограниченного контекста, каждый из которых может требовать только какое-то подмножество данных, которые возвращает данный интерфейс. И однозначно не требует ни одного метода который может изменить состояние внешнего сервиса. Кроме этого агрегату может потребоваться информация из нескольких внешних сервисов. Например для принятия решения агрегату нужны какие-то данные из банка который обслуживает клиента и из его страховой компании. Outside служит как раз для того чтобы абстрагировать эти внешние источники. Для агрегата это просто информация из внешнего мира, он не знает ничего о банках и страховых компаниях.

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

Согласен, возможно данный подход слишком пуристический, и требует создания дополнительной абстракции в виде фасада заточенного под конкретный агрегат, но на практике оверхед не такой большой как кажется :)

Ну и это не внешний агрегат скорее внешний фасад который враппит другие зависимости

По факту да, тут используется этот принцип, но статья рассчитана на людей имеющих понятие о DDD и гексагональной архитектуре, поэтому мне показалось излишним это упоминать.

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

Решил более подробно описать свой вариант 6 который мы обсуждали в комментариях https://habr.com/ru/articles/799019/

А если проверка не касается уникальности, и она опирается на данные из другого внешнего контекста?

Да, такой риск существует, именно для решения этой проблемы в DDD существуют ограниченные контексты. https://martinfowler.com/bliki/BoundedContext.html


Кроме этого, даже в рамках одного ограниченного контекста агрегат можно разбить на value objects, каждому из которых можно делегировать часть бизнес логики.

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

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

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

Интересный подход. Но если валидация происходит в доменном сервисе то что нам мешает создать сразу ValidUser и добавить его в репозиторий забыв провалидировать через доменный сервис?

Мне кажется что event handler должен находится в слое Application. Ну и я согласен с @DmitriyGordinskiy который указывает на потенциальную проблему в случае с проверками в ивент хендлерах. С моей точки зрения если агрегат опубликовал событие, это означает что его состояние изменилось и оно должно быть валидным.

Для нас этот паттерн имеет еще одно преимущество. На этапе имплементации бизнес логики, нам не нужно задумываться о том откуда конкретно мы будем получать внешние данные для принятия решений. Грубо говоря первым этапом реализации бизнес операции является написания логики внутри агрегата и объявление интерфейса outside если эта логика требует внешних данных. В процессе разработки агрегат покрывается юнит тестом, который покрывает все edge cases фичи (в отличии от подхода с проверками в слое Application, отдельных доменных сервисах или обработчиках событий), а его outside просто мокается.
После того как разработка и тестирование доменного слоя закончена, в рамках интеграционного тестирования outside подключается к уже существующим сервисам-коннекторам или создаются новые сервисы-коннекторы типа ITicketSystemGateway.

Вот пример такого теста с моком https://gitlab.com/grifix/sandbox/-/blob/main/GUIDELINE.md?ref_type=heads#unit-tests

Это не совсем то, хотя очень похоже :). Ответственностью тех сервисов которые вы описали является коммуникация с конкретной внешней службой. У outside немного другая ответственность. Он является каки бы anticorruption layer между агрегатом и теми внешними службами которые нужны агрегату для принятия решения. Outside ограничивает доступ агрегата только к тем методам которые ему нужны, и не позволяет агрегату выполнять методы с сайд эффектами. Плюс отсекает лишние данные которые не нужны агрегату.
Предположим для принятия решения о скидке нам нужно информация о стоимости товара. В случае если мы внедрим внешний сервис IProductsService::getProduct($productid):ProductDto напрямую в агрегат он нам вернет всю информацию о товаре, включая название, размер и т.п. В случае с outside мы отсекаем все что нам не нужно и возвращаем только цену IOutside::getProdcutPirce($productId):int. Дополнительным бенефитом является то что при тестировании во втором случае нам нужно замокать только значение int а не целый ProductDto

Не совсем, в данном случае агрегат не имеет доступа ко всем методам репозитория, и не имеет практической возможности вытащить другой агрегат и выполнить на нем бизнес операцию, кроме того в реализации этого интерфейса можно враппить и другие сервисы, которые предоставляют данные агрегату для принятия бизнес решений. Причем это агрегат описывает формат данных который ему нужен, таким образом он никак не зависит от этих внешних сервисов. Более подробно этот подход описан тут https://gitlab.com/grifix/sandbox/-/blob/main/GUIDELINE.md?ref_type=heads#aggregate-outside

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

<?php

#Domain
interface UserOutsideInterface
{
    public function isEmailUnique(string $email): bool;
}

final class User{

    public function __construct(
        private readonly UserOutsideInterface $outside,
        private string $email,
    )
    {
        if (!$this->outside->isEmailUnique($this->email)) {
            throw new EmailAlreadyExistsException();
        }
    }
}


#Infrastructure
final class DefaultUserOutside implements UserOutsideInterface
{
    public function __construct(
        private UserRepository $repository
    {
    }

    public function isEmailUnique(string $email): bool
    {
        return $this->repository->hasWithEmail($email);
    }
}

Information

Rating
Does not participate
Location
Warszawa, Mazowieckie, Польша
Date of birth
Registered
Activity