Общение между слоями

Обсуждаем, как правильно строить приложения
executor
Сообщения: 3
Зарегистрирован: 2018.01.14, 15:08

Общение между слоями

Сообщение executor »

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

1. Контроллер получает rawData, которые должны бросаться через Dto в сервис и там валидироваться, создавать обьекты и т.д.?
Получилась след проблема: в начале считал, что кидаем в сервис ИДшки и там уже разруливаем все (проверяем на существование, валидность, дергаем репу и .т.д). Но когда возникла потребность заюзать один сервис в другом, то получилось, что в родительском сервисе уже было все создано, ну и прокидывать отдельные поля в дочерний сервис, а потом в нем опять что-то создавать/выгребать те же записи/обьекты - не камильфо. С другой же стороны если в сервис бросать уже готовые обьекты, то создавать в контроллере тоже не правильно.

2. Как правильно прокидывать ошибки между слоями? Я так понимаю в контоллере создаем сервис, помещаем его в try{} и ловим все ошибки, которые брошены с Domain Layer & Service Layer? и это все дальше в setFlash. Если ошибка не с уровня сервиса и домена то 500?

3. В Laravel очень понравился подход с Командами. Они могут юзатся как сами по себе, выполняя рутину так и в очередях. Интересно подход Service Layers и Commands это по сути что-то сродненное ? и то и то может впитать логику из контроллеров...

Заранее спасибо за ответы.
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Общение между слоями

Сообщение ElisDN »

1 Из контроллера в сервис - только id-шники. А внутри сервисов уже без разницы. Если из контроллеров дёргаются эти A и B и надо из A дёрнуть B, то вынесите общее в доменный C и дёргайте его из A и B.

2. Да, выводим все DomainException через flash.

Код: Выделить всё

class PostController extrnds Controller
{
    public function actionCreate()
    {
        $form = new PostCreateForm();
        if ($form->load(Yii::$app->request->post()) && $form->validate()) {
            try {
                $this->service->create($form);
                return $this->redirect(['index']);
            } catch (\DomainException $e) {
                Yii::$app->errorHandler->logException($e);
                Yii::$app->session->setFlash('error', $e->getMessage());
            }
        }
        return $this->render('create', [
            'model' => $form,
        ]);
    }
}
3. Подход с CommandBus - альтернатива организации сервисов приложения., когда каждый метод выносится в отдельный класс:

Код: Выделить всё

namespace App\Command\User\Signup;

class Command
{
    public $username;
    public $email;
    public $password;
}

class Handler
{
     public function __invoke(Command $command) { ...}
}
И поиск Handler к каждой Command производится шиной команд:

Код: Выделить всё

class SimpleCommandBus implements CommandBus
{
    private $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function execute($command): void
    {
        $handler = $this->resolveHandler($command);
        $handler($command);
    }

    public function resolveHandler($command): callable
    {
        $class = (new \ReflectionClass($command))->getNamespaceName() . '\Handler';
        return $this->container->get($class);
    }
}
И теперь используем эту шину в контроллере. Например, в Symfony:

Код: Выделить всё

use App\Command\Product\Create\Command as CreateCommand;
use App\Command\Product\Create\Form as CreateForm;

class ProjectController extends Controller
{
    private $command;
    private $logger;

    public function __construct(CommandBus $command, LoggerInterface $logger)
    {
        $this->command = $command;
        $this->logger = $logger;
    }

    /**
     * @Route("/projects/create", name="projects.create")
     */
    public function create(Request $request): Response
    {
        $command = new CreateCommand($this->getUser()->getId());

        $form = $this->createForm(CreateForm::class, $command);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            try {
                $this->command->execute($command);
                return $this->redirectToRoute('projects');
            } catch (\DomainException $e) {
                $this->logger->error($e->getMessage(), ['exception' => $e]);
                $this->addFlash('error', $e->getMessage());
            }
        }

        return $this->render('app/projects/create.html.twig', [
            'form' => $form->createView(),
        ]);
    }
}
executor
Сообщения: 3
Зарегистрирован: 2018.01.14, 15:08

Re: Общение между слоями

Сообщение executor »

1. Ну вот к примеру у меня сервис OrderStatusService, который меняет статус, отправляет нотификации, и изменяет рейтинг исполнителя (UserRatingService) в зависимости от успешности или нет.
UserRatingService я также юзаю просто в отдельном экшене - изменение рейтинга пользователя при других условиях .
Получается если я в UserRatingService и OrderStatusService бросаю просто id пользователя, то в первой ситуации я два раза должен выгребти одного и того же пользователя, для манипуляций с заказом, и для простановки рейтинга во вложеном сервисе. А если бы еще какой-то был вложенный сервис... или нужно было бы еще по id что-то извлекать получается как-то накладно.
2. с сервисов тоже отображаем как пользовательские ошибки?
3. Если речь не идет про использование в очередях, можно адаптировать команды, чтобы они могли возвращать данные и бросали исключения? аля маленькие автоматизированные сервисы?
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Общение между слоями

Сообщение ElisDN »

1. Не надо "контроллерные" сервисы вызывать друг из друга. Если это простой перерасчёт, то в OrderStatusService и UserRatingService дёргайте $user->changeRating(...). Если рейтинг считается сложнее, то сделайте вложенный RatingCalculator и вызывайте из обоих $this->calc->calcRating($user).

2. Если сервис кинет \DomainException, то также и выведется.

3. Всё можно.
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Общение между слоями

Сообщение anton_z »

executor писал(а): 2018.01.15, 22:40 1. Ну вот к примеру у меня сервис OrderStatusService, который меняет статус, отправляет нотификации, и изменяет рейтинг исполнителя (UserRatingService) в зависимости от успешности или нет.
UserRatingService я также юзаю просто в отдельном экшене - изменение рейтинга пользователя при других условиях .
Получается если я в UserRatingService и OrderStatusService бросаю просто id пользователя, то в первой ситуации я два раза должен выгребти одного и того же пользователя, для манипуляций с заказом, и для простановки рейтинга во вложеном сервисе. А если бы еще какой-то был вложенный сервис... или нужно было бы еще по id что-то извлекать получается как-то накладно.
На мой взгляд, у Вас ключевой момент здесь - смена статуса заказа. Я бы внутри Order::changeStatus() все сделал и обошелся бы без сервисов. Шину событий и др. зависимости дернул бы через синглтон Yii::$app в init() (если Yii) или __construct(). Получилось бы все близко к понятиям предметной области. У чего меняется статус - у заказа - значит операция changeStatus() - и она должна быть самодостаточной, а не работать только при вызове из определенного сервиса.

Код: Выделить всё

class Order  extends ActiveRecord
{

    private $event_manager;
    
    //это вместо внедрения через конструктор, мокабельно
    public function init()
    {
        $this->event_manager = \Yii::$container->get(EventManager);
    }
    
    
    public function changeStatus()
    {
        $this->status = $some_status;
        $this->update(['status']);
        
        //user может быть описан в relations, либо вы его можете вытянуть по своим соображениям
        $this->user->changeRating();
        
        $this->event_manager->trigger(new StatusChangedEvent);
    }
}

В User::changeRating() - аналогчно - полностью прописал бы всю логику операции с зависимостями и нотификациями.

Итого: нет проблемы с повторным обращением к базе, User::changeRating() и Order::changeStatus() реюзабельны в разных точках приложения без всязких сервисов. Как сдествие, не возникает проблемы с декомпозицией этих самых сервисов, что очень похоже на декомпозицию процедур из процедурного программирования.
executor
Сообщения: 3
Зарегистрирован: 2018.01.14, 15:08

Re: Общение между слоями

Сообщение executor »

Да, видимо сильно увлекся разнесение кода, что намудрил с сервисами
$user->changeRating(...)
Здесь же можно работать только с таблицей user или со связанными тоже допустимо?
Можно ли в таких методах модели делать небольшую валидацию, скажем на право пользователя изменить рейтинг/статус, и бросать DomainExeption?
В User::changeRating() - аналогчно - полностью прописал бы всю логику операции с зависимостями и нотификациями.
Ну это как-то сильно хардкорно и не по ООП. если модели начнут слать уведомления + станет невозможным реализовать правильную работу транзакций: где-то закрашится метод какой-то модели, а сообщения уже улетят.
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Общение между слоями

Сообщение anton_z »

executor писал(а): 2018.01.17, 08:46 Здесь же можно работать только с таблицей user или со связанными тоже допустимо?
Допустимо. Почему нет? Тем более что у вас исполнитель и по реляции доступен, есть явная связь как семантически, так и по данным.
executor писал(а): 2018.01.17, 08:46 Можно ли в таких методах модели делать небольшую валидацию, скажем на право пользователя изменить рейтинг/статус, и бросать DomainExeption?
Да, я так тоже делаю.
executor писал(а): 2018.01.17, 08:46 Ну это как-то сильно хардкорно и не по ООП. если модели начнут слать уведомления + станет невозможным реализовать правильную работу транзакций: где-то закрашится метод какой-то модели, а сообщения уже улетят.
Я не это имел ввиду. Уже давно на этом форуме обсуждали как слать разные нотификации из транзакции, и у Вернона это в книге есть. Заводишь в своей реляционной базе (на которой и выполняется транзакция) специальную таблицу StoredEvent. В транзакции туда пишешь что произошло такое-то событие (UserRatingChanged), например при выполнении метода User::changeRating(). Потом кроном или демоном каким-нибудь из этой таблицы вытаскиваешь необработанные события и шлешь по ним вче что надо.
Если произойдет ошибка или исключение - транзакция откатится, ничего никуда не пойдет. Запись в StoredEvent станет видна для другого процесса только после успешного коммита.
У нас основной источник событий всяких это модели, кому, если не им их генерировать. А вот слать почту по этим событиям должны обработчики. Т.е. спагетти--кода не будет.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Общение между слоями

Сообщение sda »

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

Один минус. События теперь всегда надо цеплять к какой-то сущности. Не получится например бросить событие из доменного сервиса. Как думаете нормальное решение ?
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Общение между слоями

Сообщение anton_z »

sda писал(а): 2018.01.17, 11:29 Один минус. События теперь всегда надо цеплять к какой-то сущности. Не получится например бросить событие из доменного сервиса. Как думаете нормальное решение ?
Если я правильно понял, вы всегда одним запросом к одному документу в монге одновременно меняете данные документа и добавляете к нему события (как вложенные документы), верно? Если так, то все в порядке, консистентность будет, ведь изменения в одном документе в рамках одного запроса атомарны. Классно вы придумали, я запомню ваше решение.
Ну а по поводу событий не из сущностей - то тут либо попробовать подобрать сущность, либо исхитряться и переделывать функционал чтобы не было мофикаций более одного документа, либо менять СУБД на реляционную (что конечно, слишком рискованно, можно сроки все сорвать). Мы по причине отсутствия транзакций, монгу в качестве основного хранилища не взяли, используем как распределенное хранилище для файлов.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Общение между слоями

Сообщение sda »

anton_z писал(а): 2018.01.17, 12:32 Если я правильно понял, вы всегда одним запросом к одному документу в монге одновременно меняете данные документа и добавляете к нему события (как вложенные документы), верно?
да
Ну а по поводу событий не из сущностей - то тут либо попробовать подобрать сущность, либо исхитряться и переделывать функционал чтобы не было мофикаций более одного документа
Следую правилу не сохранять более одной сущности. Еще я нашел у Вернона, что если событие надо бросить не из сущности, то само событие надо сделать агрегатом со своим репозиторием. Вот цитаты
Sometimes Events are designed to be created by direct request from clients. This is done in response
to some occurrence that is not the direct result of executing behavior on an instance of an Aggregate in
the model. Possibly a user of the system initiates some action that is considered an Event in its own
right. When that happens, the Event can be modeled as an Aggregate and retained in its own Repository.
Since it represents some past occurrence, its Repository would not permit its removal.

When an Event is modeled in this fashion, it can be published via messaging infrastructure at the
same time as it is added to its Repository. The client could call on a Domain Service (7) to create the
Event, add it to its Repository, and then publish it over a messaging infrastructure.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Общение между слоями

Сообщение sda »

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

Надо еще чтобы при дублях сообщений новая сущность всегда пыталась вставиться в базу с тем же самым идентификатором, чтобы монга всегда пыталась записать документ с одним и тем же идентификатором и вываливалась бы с DuplicateKeyException.
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Общение между слоями

Сообщение anton_z »

Мне кажется что вам надо просто сделать последовательные id для вложенных документов на основе вызова findAndModify() и дедуплицировать на их основе (запоминать значение счетчика). Атомарность между инкрементом счетчика и изменениями в документе не нужна.

Код: Выделить всё


public function activate() 
{
   $id = $this->nextEventId();
   $this->state = self::STATE_ACTIVE;
   $this->update(['state' => $this->state, 'events' => ['$addToSet' => ['event_id' => $id, 'type' => SomethingActivatedEvent::TYPE, 'payload' => $payload]]])

}
С вызовами мог напутать, специально в документацию лезть не хочется, смысл должен быть понятен.

nextEventId() работает на основе findAndModify() на отдельной коллекции - последовательность id общая для всех документов.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Общение между слоями

Сообщение sda »

дедуплицировать на их основе (запоминать значение счетчика)
Как ? Что-то пока не могу понять идею.
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Общение между слоями

Сообщение anton_z »

sda писал(а): 2018.01.19, 03:09
дедуплицировать на их основе (запоминать значение счетчика)
Как ? Что-то пока не могу понять идею.
Слушатели могут запоминать, id последнего обработанного события. Так как id возрастает, можно принять все события с id меньшим последнего обработанного обработанными и пропускать их. Значение счетчика можно хранить в бд или в файле.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Общение между слоями

Сообщение sda »

anton_z, но слушатели не могут атомарно запомнить id и выполнить операцию. Если увеличить id до операции, тогда операция может быть не выполнена, если увеличивать после операции, тогда операция может быть выполнена более 1 раза.
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Общение между слоями

Сообщение anton_z »

Тут уже ничего не поделаешь,. надо делать слушателей идемпотентными. Если в редчайших случаях будет два имейла я думаю ничего страшного. А какие у вас слушатели?
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Общение между слоями

Сообщение sda »

anton_z, вот такие. Они вызывают сервисы приложения и меняют состояние сущностей. Отправить email 2 раза ничего страшного. Изменить состояние сущности 2 раза это уже нарушение согласованности данных. Сделать всю доменную модель идемпотентной невозможно. Сделать идемпотентным сам слушатель тоже невозможно так как отсутствуют транзакции.
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Общение между слоями

Сообщение anton_z »

Ну что я могу сказать, вам нужны транзакции - без них не достичь согласованности между счетчиком обработанных событий и изменениями данных. Можно попробовать с помощью двухфазного коммита их эмулировать на монге - но это сложно и хрупко.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Общение между слоями

Сообщение sda »

anton_z, а вы как делаете? Меняете все агрегаты в одной транзакции и сохраняете в рсубд ?
anton_z
Сообщения: 483
Зарегистрирован: 2017.01.15, 15:01

Re: Общение между слоями

Сообщение anton_z »

Да, я использую транзакции. Это важнейший элемент для обеспечения согласованности. Если в обработчике события нужно поменять данные моделей атомарно с сохранением счётчика , делаю это в транзакции.
Ответить