Сервисный слой, как правильно?

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

Re: Сервисный слой, как правильно?

Сообщение ElisDN »

Melodic писал(а):И что бы сделать array_diff, нужно будет ещё раз загрузить Order?
Можно дёрнуть айдишники SELECT product_id для array_diff перед сохранением.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

zelenin писал(а): Опять же вы не сможете создать сущность, не сгенерировав id в приложении (мы общались об этом в личке).
Т.е. создавать новую сущность через

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

new User() 
не правильно? В конструктор должен сразу id передаваться?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

Melodic писал(а):
zelenin писал(а): Опять же вы не сможете создать сущность, не сгенерировав id в приложении (мы общались об этом в личке).
Т.е. создавать новую сущность через

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

new User()
не правильно? В конструктор должен сразу id передаваться?
сущность - это валидный объект с уникальной идентичностью (ID).
пример: $coder = new User(); $project->addCoder($coder); $project->addCoder($coder); - второе добавление кодера должно выкинуть эксепшн, т.к. у нас не может быть в проекте один кодер два раза. А проверить его уникальность мы не можем, т.к. сущность Кодер на данный момент не обладает индентичностью. Поэтому создаем сущность мы уже полностью готовой.

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

$coder = new User($userRepo->nextIdentity(), $name);
$project->addCoder($coder); 
В $userRepo->nextIdentity() вы либо генерите uuid либо вытаскиваете следующий id из базы. uuid универсальнее.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

На сколько мне известно, из БД нельзя получить следующий Id, id известен только после вставки записи.

Да и как писал в личке, если сущности будут сохранятся не в БД, а где то через API, то следующий ID будет проблемно узнать.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

Melodic писал(а):На сколько мне известно, из БД нельзя получить следующий Id, id известен только после вставки записи.
http://stackoverflow.com/questions/6761 ... d-in-mysql
Melodic писал(а):Да и как писал в личке, если сущности будут сохранятся не в БД, а где то через API, то следующий ID будет проблемно узнать.
в апи нет сущностей. Через апи мы кидаем DTO, а на стороне сервера в Application слое уже из DTO формируем сущность с ID.

Но такой способ конечно крайне не надежный. Шанс получить один id для двух почти одновременно создаваемых сущностей очень велик.
Поэтому Uuid. https://github.com/zelenin/ddd-core/blo ... erator.php
nootropil
Сообщения: 46
Зарегистрирован: 2015.11.21, 18:45

Re: Сервисный слой, как правильно?

Сообщение nootropil »

zelenin писал(а):Но такой способ конечно крайне не надежный. Шанс получить один id для двух почти одновременно создаваемых сущностей очень велик.
Поэтому Uuid. https://github.com/zelenin/ddd-core/blo ... erator.php
А не будет ли проблем с произодительностью базы данных из-за Uuid?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

nootropil писал(а):
zelenin писал(а):Но такой способ конечно крайне не надежный. Шанс получить один id для двух почти одновременно создаваемых сущностей очень велик.
Поэтому Uuid. https://github.com/zelenin/ddd-core/blo ... erator.php
А не будет ли проблем с произодительностью базы данных из-за Uuid?
последовательные (есть такая оптимизация) uuid по скорости сравнимы с автоинкрементом, по размеру на жестком диске проигрывают 30%.
Но это все это неважно на маленьком проекте, а на большом проекте для чтения юзаются read-движки (не sql) с моментальным доступом.
nootropil
Сообщения: 46
Зарегистрирован: 2015.11.21, 18:45

Re: Сервисный слой, как правильно?

Сообщение nootropil »

zelenin писал(а): последовательные (есть такая оптимизация) uuid по скорости сравнимы с автоинкрементом, по размеру на жестком диске проигрывают 30%.
Но это все это неважно на маленьком проекте, а на большом проекте для чтения юзаются read-движки (не sql) с моментальным доступом.
Спасибо. В DDD мы можем работать с автоинкрементным идентификатором?

А вообще, тема очень интересная и мне нужная.
Так как абстрактно всё сразу понять сложно, поробовал реализовать на примере регистрации и восстановления аккаунта.
Вороде на слои что-то разложить получлось, но уверенности что это DDD - нет.

Буду благодарен за адекватную критику:
PS Осторожно, много плагиата :D
https://bitbucket.org/nootropil/studyin ... ?at=master
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

nootropil писал(а):Спасибо. В DDD мы можем работать с автоинкрементным идентификатором?
да. Генерацией id занимается хранилище (репозиторий). Метод nextIdentity может возвращать сегенерированный приложением uuid либо вытащенный из базы следующий id, но это race condition. Без генерации id в приложении уже некрасиво. Поэтому я бы использовал uuid и не извращался.
nootropil писал(а):Так как абстрактно всё сразу понять сложно, поробовал реализовать на примере регистрации и восстановления аккаунта.
Вороде на слои что-то разложить получлось, но уверенности что это DDD - нет.
это прикладная задача - лучше на нормальной доменной проблеме пробовать, например посты, теги, авторы - написание блога для практики думаю самое то.
nootropil писал(а):Буду благодарен за адекватную критику:
PS Осторожно, много плагиата :D
https://bitbucket.org/nootropil/studyin ... ?at=master
https://bitbucket.org/nootropil/studyin ... ser.php-86
зачем скаляр возвращать? скаляр нужен только для хранения в базе.

https://bitbucket.org/nootropil/studyin ... ser.php-96
в ddd не используются тупые сеттеры. Методы должны быть названы более человекопонятно - rename например, changePassword, activize. Некоторые подобные методы могут быть сеттерами по сути, а могут объединять некое бизнес правило типа "Одобрить юзера" - проставить статус, изменить роль, увеличить рейтинг на один балл.

https://bitbucket.org/nootropil/studyin ... er.php-249
не уверен, что токены должны быть вообще частью домена. Активировать юзера мы можем разными способами - токен не обязательная часть юзера. Можно вынести в другой модуль типа Auth. Аналогично passwordResetToken и соответствующие методы в репозитории, и сервисы рассылок писем.

https://bitbucket.org/nootropil/studyin ... ew-default
команда - это просто сообщение, выполняемое command handler'ом, а не самой собой. Команды - это часть слоя Приложения - не домена.
Мы получили в контроллере какие-то данные (из формы или по api), сформировали из request команду и кинули ее куда-то на выполнение (в шину, сервис или хэндлер) - это уровень Приложения. А вот внутри хэндлера мы уже команды преобразовываем в домен на основе некоторой логики - извлекаем модель из репозитория, с помощью методов меняем ее, с помощью других сервисов рассылаем письма итд.

https://bitbucket.org/nootropil/studyin ... ew-default
репозиторий - это не сервис. Домен состоит из трех частей - модели (сущности и value objects), сервисы домена, репозитории.

https://bitbucket.org/nootropil/studyin ... ace.php-28
зачем add, если есть save? можно в save делать все проверки и на стороне базы уже либо инсертить либо апдейтить.

https://bitbucket.org/nootropil/studyin ... ?at=master
все команды у вас на самом деле сервисы слоя Приложение. Рекомендую вынести туда и разделить на Command и Command Handler/Application Service.

https://bitbucket.org/nootropil/studyin ... and.php-72
опять же сеттеры не юзаем, а юзаем например фабрики для создания юзера в контексте регистрации (User::signupUser(...)/UserFactory::signupUser(..)) - делаем домен более "говорящим".

https://bitbucket.org/nootropil/studyin ... ew-default
в домене у нас интерфейсы сервисов с одним методом, а реализации в инфраструктуре. Но опять же сам этот сервис - сервис приложения, который должен юзать сущности модулей User и Auth.

https://bitbucket.org/nootropil/studyin ... ry.php-161
логично всю логику по созданию модели вынести в UserFactory::create(..), чтобы не размазывать по всему приложению правила, по которым модель создается - инкапсулируем в фабрику.

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

Re: Сервисный слой, как правильно?

Сообщение ElisDN »

nootropil писал(а):Так как абстрактно всё сразу понять сложно, поробовал реализовать на примере регистрации и восстановления аккаунта.
Вороде на слои что-то разложить получлось, но уверенности что это DDD - нет. Буду благодарен за адекватную критику:
1. В сущности User у Вас практически только геттеры и сеттеры. Из-за этого сами работаете с ней процедурно, а не как с объектом. Сеттеров в сущности быть практически не должно.

2. Вывернутая структура директорий. Удобнее наоборот Model\User.

3. Команды относятся к слою приложения. Так что их лучше переложить в Application\Command. И помещать внутрь них метод execute неудобно, так как приходится возиться с конструктором при их создании. Удобнее разбить на пустой класс-DTO с полями XxxCommand и обработчик XxxCommandHandler, чтобы шина команд их сопоставляла.

P.S. Опоздал с ответом :)
nootropil
Сообщения: 46
Зарегистрирован: 2015.11.21, 18:45

Re: Сервисный слой, как правильно?

Сообщение nootropil »

zelenin писал(а): команда - это просто сообщение, выполняемое command handler'ом, а не самой собой. Команды - это часть слоя Приложения - не домена.
Мы получили в контроллере какие-то данные (из формы или по api), сформировали из request команду и кинули ее куда-то на выполнение (в шину, сервис или хэндлер) - это уровень Приложения. А вот внутри хэндлера мы уже команды преобразовываем в домен на основе некоторой логики - извлекаем модель из репозитория, с помощью методов меняем ее, с помощью других сервисов рассылаем письма итд.
ElisDN писал(а): 3. Команды относятся к слою приложения. Так что их лучше переложить в Application\Command. И помещать внутрь них метод execute неудобно, так как приходится возиться с конструктором при их создании. Удобнее разбить на пустой класс-DTO с полями XxxCommand и обработчик XxxCommandHandler, чтобы шина команд их сопоставляла.
Что то совсем с командами потерялся. Гугл предлагает обрабатывать всё в комманде методами или "execute" или "handle".

Не совсем понятно как передавать зависимости в handler, такой вариант, как понимаю, не правильный (всё публично):
PS Пока без DTO

https://bitbucket.org/nootropil/studyin ... ew-default
https://bitbucket.org/nootropil/studyin ... ew-default
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Сервисный слой, как правильно?

Сообщение ElisDN »

nootropil писал(а):Не совсем понятно как передавать зависимости в handler
Пример шины приводил здесь на четвёртой странице. Зависимости принимать через конструктор:

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

class SignUpCommand
{
    public $username;
    public $email;
    public $password;

    public function __construct($username,  $email, $password)
    {
        $this->username = $username;
        $this->email = $email;
        $this->password = $password;
    }
}

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

class SignUpHandler
{
    private $userRepository;
    private $confirmTokenRepository;
    private $passwordHasher;
    private $userNotificator;

    public function __construct(
        UserRepository $userRepository,
        ConfirmTokenRepository $confirmTokenRepository,
        PasswordHasherInterface $passwordHasher,
        UserNotificationInterface $userNotificator,
    )
    {
        $this->userRepository = $userRepository;
        $this->confirmTokenRepository = $confirmTokenRepository;
        $this->passwordHasher = $passwordHasher;
        $this->userNotificator = $userNotificator;
    }

    public function handle(SignUpCommand $command)
    {
        $this->guardUsernameIsUnique($command->username);
        $this->guardEmailIsUnique($command->email);

        $userId = $this->userRepository->nextIdentity();
        $user = User::singUp(
            $userId,
            $command->username,
            $this->passwordHasher->hash($command->password),
            $command->email
        );
        $this->userRepository->add($user);

        $tokenId = $this->confirmTokenRepository->nextIdentity();
        $token = ConfirmToken::create(
            $tokenId,
            $userId
        );
        $this->confirmTokenRepository->add($token);

        $this->userNotificator->sendActivationEmail(
            $user->getEmail(),
            $user->getUsername(),
            $token->getToken()
        );
    }
    
    ...
} 

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

class PasswordHasher implements PasswordHasherInterface
{
    public function hash($password)
    {
        return Yii::$app->security->generatePasswordHash($password);
    }

    public function validate($password, $hash)
    {
        return Yii::$app->security->validatePassword($password, $hash);
    }
} 
Последний раз редактировалось ElisDN 2016.07.25, 16:38, всего редактировалось 5 раз.
nootropil
Сообщения: 46
Зарегистрирован: 2015.11.21, 18:45

Re: Сервисный слой, как правильно?

Сообщение nootropil »

ElisDN писал(а): Пример шины приводил здесь на четвёртой странице. Зависимости принимать через конструктор:
Я не правильно вопрос задал, но ваш ответ всё равно натолкнул меня на решение ;)

Примерно так должно быть в контроллере?

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

class SignUpCommand
    public function actionXxx()
    {
     ...
        $signUpDto = new SignUpDto(
            $username,
            $email,
            $password
        );

        $comBus = new CommandBus();
        $comBus->execute(
            new SignUpCommand($signUpDto),
            new SignUpHandler(
                new UserRepository(),
                new UserReadRepository(),
                new ConfirmTokenRepository(),
                new PasswordHasherInterface(),
                new UserNotificationInterface()
            )
        );
     ....    

    }
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

типа того.
- class SignUpCommand читать как class SignUpController
- $comBus = new CommandBus(); логично ее откуда-то из di получить
- new SignUpHandler и соответственно шина внутри себя пусть инкапсулирует подбор хэндлера к команде
- а вообще DTO в данном случае ненужное промежуточное звено - сразу формируйте команду из данных из реквеста.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

nootropil писал(а):Что то совсем с командами потерялся. Гугл предлагает обрабатывать всё в комманде методами или "execute" или "handle".
это ларавельные псы искажают концепцию, скорее всего.
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Сервисный слой, как правильно?

Сообщение ElisDN »

nootropil писал(а):Примерно так должно быть в контроллере?
Команда - это и есть DTO. Формируем команду и кидаем в execute. А commandBus сама найдёт хэндлер по get_class($command) и выполнит $handler->handle($command).

Вот весь контроллер:

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

class UserController
{
    private $commandBus;

    public function __construct($id, $module, CommandBusInterface $commandBus, $config = [])
    {
        $this->commandBus = $commandBus;
        parent::__construct($id, $module, $config);
    }
    
    public function actionSignup()
    {
        $form = Yii::createObject(SignupForm::className());
        if ($form->load(Yii::$app->request->post()) && $form->validate()) {
            $this->commandBus->execute(new SignUpCommand(
                $form->username,
                $form->email,
                $form->password
            ));
            Yii::$app->session->setFlash('success', 'Please confirm your Email.');
            return $this->goHome();
        }
        return $this->render('signup', [
            'signupForm' => $form,
        ]);
    }
} 
Саму CommandBus берём и регистрируем, например, как здесь.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

Re: Сервисный слой, как правильно?

Сообщение Melodic »

Дмитрий, вы предлагали в view передавать dto, у меня получилось как то так:

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

public function actionView($id)
    {
        $article = $this->loadArticle($id);
        if ($article->isPublished()) {
            $originals = [];
            $thumbnails = [];
            foreach ($article->images as $image) {
                /**
                 * @var $image GalleryImage
                 */
                $thumbnails[] = $this->imageService->thumbnail($this->galleryService->path($image->file), $this->thumbnailWidth, $this->thumbnailHeight, Image::CROP);
                $originals[] = $this->imageService->thumbnail($this->galleryService->path($image->file), $this->originalWidth, $this->originalHeight, Image::CROP);
            }
            $this->apiService->loadProfileAdditionalFields([$article->authorProfile]);
            $this->articleService->view($article, \Yii::$app->request->userIP);
            return $this->render('view', [
                'dto' => new ArticleViewDto(
                    ProfileHelper::getFullName($article->authorProfile),
                    $article->authorProfile->avatar,
                    \Yii::$app->user->id === $article->user_id,
                    $article->id,
                    $article->header,
                    $article->text,
                    $article->approved_at,
                    $this->articleService->views($article),
                    $this->recommendService->countRecommends($article),
                    $this->commentService->countComments($article),
                    $thumbnails,
                    $originals,
                    $this->tagService->tags($article),
                    \Yii::$app->user->isGuest ? false : $this->favoriteService->isFavorite($article, \Yii::$app->user->identity),
                    \Yii::$app->user->isGuest ? false : $this->recommendService->isRecommended($article, \Yii::$app->user->identity)
                )
            ]);
        } else {
            throw  new NotFoundHttpException();
        }
    } 
Как то громоздко получилось или это нормально?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

громоздкость - это же не вопрос dto.
вообще суть такая:

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

$dto = ArticleDto::createFromRequest(...);
$this->service->handleDto($dto); // тут ValidationException если что.
$this->render('...',['dto' => $dto]); 
все остальное - в сервисы.
nootropil
Сообщения: 46
Зарегистрирован: 2015.11.21, 18:45

Re: Сервисный слой, как правильно?

Сообщение nootropil »

Упростил задачу что бы сконцентироваться на том "как" делать. Попробовал максимально учесть накопленный опыт этого раздела. Реализован CRUD функционал сущности "Юзер". Пример рабочий.

Буду благодарен за адекватную критику:
https://bitbucket.org/nootropil/studyin ... ?at=master
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Сервисный слой, как правильно?

Сообщение zelenin »

nootropil писал(а):Упростил задачу что бы сконцентироваться на том "как" делать. Попробовал максимально учесть накопленный опыт этого раздела. Реализован CRUD функционал сущности "Юзер". Пример рабочий.

Буду благодарен за адекватную критику:
https://bitbucket.org/nootropil/studyin ... ?at=master
1. https://bitbucket.org/nootropil/studyin ... er.php-120

формат ни к чему. Можно сделать во время записи в базу.

2. https://bitbucket.org/nootropil/studyin ... ?at=master

шина - часть слоя Application (также query bus)

3. https://bitbucket.org/nootropil/studyin ... ew-default

лучше модели отдельно, репозитории отдельно - разделить директории

4. https://bitbucket.org/nootropil/studyin ... ew-default

гидрация - часть реализации хранилища (репозитория), т.е. инфраструктуры. для домена есть только абстрактное хранилище.

5. https://bitbucket.org/nootropil/studyin ... ew-default

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

https://bitbucket.org/nootropil/studyin ... ory.php-47

тут казалось бы нету протечки, т.к. Query юзается в качестве библиотеки, но на самом деле Query внутри себя юзает опять же инстанс yii, получая доступ к db-коннекшну. Тоже протечка.

в прочем использование везде Yii::createObject оже протечка.

6. https://bitbucket.org/nootropil/studyin ... ew-default

дело вкуса, но я бы делал все-таки приватными поля и геттеры. либо публичные поля и констуктор в виде __construct(array $attributes)

а вообще я смотрю у вас и в хэндлерах поля публичные. Вообще публичность - это нарушение инкапсуляции, поэтому все поля по умолчанию приватные. Публичные поля нужны в редких случаях, например в DTO.

7. https://bitbucket.org/nootropil/studyin ... ler.php-47

вот эти все вызовы методов лучше вынести в фабрику или модель, чтобы они автоматически вызвались при регистрации (или какой у вас там кейс).
Смотрите, у нас модель богатая поведением. У нас метод activate() называется так не потому, что нужно просто переименовать setActive(), а потому что activate() у нас теперь обладает поведением, и не только может проставлять статус, но и заодно присваивать роль, проставлять дату итд в зависимости от вашего бизнес-кейса. Выносите максимум бизнес-задач в модель, делайте ее богатой.

8. https://bitbucket.org/nootropil/studyin ... ew-default

view - часть слоя Presentation

9. https://bitbucket.org/nootropil/studyin ... ?at=master

query (и command) - это часть парадигмы message driven design. Это сообщения императивного типа, т.е. приказ что-то выполнить. Поэтому назваться должны так: Создать юзера - CreateUser, Получить юзера - GetUser/GetUserById итд. ну а суффикс Command/Query - дело вкуса.
Закрыто