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

Обсуждаем, как правильно строить приложения
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

viewtopic.php?f=19&t=36725&e=1&view=unread#p188512
по п.3: реализация базовая - нужно учесть, что есть ситуации, когда необходимо ресолвить и зависимости зависимостей.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

zelenin писал(а):
Melodic писал(а):Если UserServiceInterface относится к Application, то что тогда может являться доменным сервисом на примере пользователя?
сервис, имеющий значение для домена - генератор id, хэшер паролей, расчет комиссии - то, о чем знает домен, и что является непосредственной частью домена.
UserServiceInterface - это сервис, который данные из реквеста, преобразует в данные для домена с использованием некоей логики - другой слой. Реквест не является частью домена.
Melodic писал(а):По CommandBus. Т.е. хендлеры по сути являются сервисами слоя Application?
верно. dto превращаются в Command, а сервисы в хэндлеры.
Можно UserServiceInterface упразднить вообще и разбить его на хендлеры (UserRegistrationHandler,UserLoginHandler и т.д.)?
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

zelenin писал(а):viewtopic.php?f=19&t=36725&e=1&view=unread#p188512
по п.3: реализация базовая - нужно учесть, что есть ситуации, когда необходимо ресолвить и зависимости зависимостей.
Можно же просто ProxyManager подключить (вы ссылку давали на github), там вроде ничего сложного нет.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

Melodic писал(а):
zelenin писал(а):viewtopic.php?f=19&t=36725&e=1&view=unread#p188512
по п.3: реализация базовая - нужно учесть, что есть ситуации, когда необходимо ресолвить и зависимости зависимостей.
Можно же просто ProxyManager подключить (вы ссылку давали на github), там вроде ничего сложного нет.
Либо он не будет instanceof User либо не будет работать с final. Попробуйте, расскажите. У меня пока времени нет на практические исследования.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

Ещё по CommandBus вопрос.
CommandBus после выполнения команды не должна же ничего возвращать? Как тогда узнать об успешном выполнении? К примеру о успешном выполнении входа? С помощью событий?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

Melodic писал(а):Ещё по CommandBus вопрос.
CommandBus после выполнения команды не должна же ничего возвращать? Как тогда узнать об успешном выполнении? К примеру о успешном выполнении входа? С помощью событий?
все, что не может вернуть результат, может выкинуть исключение при ошибке
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

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

Сообщение ElisDN »

Melodic писал(а):Можно UserServiceInterface упразднить вообще и разбить его на хендлеры (UserRegistrationHandler,UserLoginHandler и т.д.)?
Да, так и делают. Вместо держания десятков методов в огромном UserService каждый операционный метод сервиса:

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

class UserSignupDto { ... }

class UserService
{
    public function signup(UserSignupDto $dto) { ... }
    public function update(UserUpdateDto $dto) { ... }
    ...
    public function ban(UserBanDto $dto) { ... }
}
подключаемого в контроллере:

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

$this->userService->signup(new UserSignupDto($form->email, $form->password));
переезжает в отдельный класс:

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

class UserSignupCommand { ... }

class UserSignupHandler
{
    public function handle(UserSignupCommand $command) { ... }
}
В итоге у нас убирается огромный UserService и десятками зависимостей в конструкторе и появляется набор примитивных классов на каждую операцию, где теперь невозможно заблудиться:

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

Command
    User
        Signup
            UserSignupCommand.php
            UserSignupHandler.php
            UserSignupValidator.php
        Ban
            UserBanCommand.php
            UserBanHandler.php
            UserBanValidator.php
Теперь в контроллер подключается только общая на весь сайт шина и в неё кидаются все команды:

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

$this->commandBus->execute(new UserSignupCommand($form->email, $form->password));
А класс шины в своём методе execute будет автоматически конструировать и запускать нужный обработчик:

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

interface CommandBusInterface
{
    public function execute($command);
}

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

class SimpleCommandBus implements CommandBusInterface
{
    public function execute($command)
    {
        $handler = $this->resolveHandler($command);
        call_user_func($handler, $command);
    }

    private function resolveHandler($command)
    {
        return [\Yii::createObject(substr(get_class($command), 0, -7) . 'Handler'), 'handle'];
    }
}
А чтобы не вызывать валидаторы в самих хендлерах, можно обернуть эту шину в другую, которая будет аналогично находить и запускать нужный для команды валидатор:

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

class UserSignupValidator
{
    private $userRepository;

    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function validate(UserSignupCommand $command)
    {
        $errors = [];

        ...

        if ($this->userRepository->existsByUsername($command->username)) {
            $errors['username'][] = 'This username has already been taken.';
        }

        if ($this->userRepository->existsByEmail($command->email)) {
            $errors['email'][] = 'This email has already been taken.';
        }

        return $errors;
    }
} 

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

class ValidationCommandBus implements CommandBusInterface
{
    private $next;
    
    public function __construct(CommandBusInterface $next) {
        $this->next = $next;
    }
    
    public function execute($command) {
        $validator = $this->resolveValidator($command);
        if ($errors = call_user_func($validator, $command)) {
            throw new ValidationException($errors);
        }
        $this->next->execute($command);
    }

    private function resolveValidator($command) {
        return [\Yii::createObject(substr(get_class($command), 0, -7) . 'Validator'), 'validate'];
    }
}
Аналогично можно cделать абсолютно любую обёртку. Например, для логов:

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

class LogCommandBus implements CommandBusInterface
{
    private $next;

    public function __construct(CommandBusInterface $next) {
        $this->next = $next;
    }
    
    public function execute($command) {
        Yii::info('Executing of ' . get_class($command), 'bus');
        $this->next->execute($command);
    }
}
И подключить обёртки в контейнере:

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

$container->setSingleton('app\commands\CommandBusInterface', function () {
    return new LogCommandBus(new ValidationCommandBus(new SimpleCommandBus()));
});
При такой структуре имеем горсть полноценных, самодостаточных и легко тестируемых примитивных классов с простыми конструкторами (у хэндлеров) вместо одного монструозного конструктора UserService. Теперь UserService можно либо удалить, либо оставить в нём только геттеры вроде getNewUsers($limit) и т.п.

Но и от геттеров подобным образом можно избавиться, если рядом с CommandBus аналогично сделать QueryBus:

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

$newUsers = $this->queryBus->query(new NewUsersQuery(10));
и производить выборки в хэндлерах NewUsersHandler.

Вот, собственно, весь полноценный рабочий CommandBus мы здесь в одном комментарии и сочинили.
Последний раз редактировалось ElisDN 2016.08.05, 23:02, всего редактировалось 4 раза.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

zelenin писал(а):
Melodic писал(а):С репозиториями не много не понятно.
По вашему совету, все методы для сохранения\добавления\обновления БД я вынес в репозиторий UserRepository.
У модели User есть поле $city, которое содержит модель City.
В репозитории UserRepository есть метод findById($id), который ищет пользователя по Id и заполняет модель User.
Проблема в том, что поле $city остаётся не заполненным. Джоинить таблицу `city` прям в методе UserRepository::findById($id) и там создавать модель City? Но это, вроде, не правильно, что UserRepository оперирует моделью City. Как быть в таком случае?
это ок. в ddd есть понятие bounded context (сущность со всеми связями) - репозиторий создается именно для bounded context.
Как будет правильно сделать?

Передавать CityRepositoryInterface ( у которого есть метод ::load($array) который возвращает модель) в UserRepository и с помощью CityRepositoryInterface::load() загружать модель City?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

я спутал термины - bounded context => aggregate root
"это ок. в ddd есть понятие aggregate root (сущность со всеми связями) - репозиторий создается именно для aggregate root."
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

вчера провел аналитику проксирования - ни окрамиус ни доктрина не проксируют final.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

Melodic писал(а):
zelenin писал(а):
Melodic писал(а):С репозиториями не много не понятно.
По вашему совету, все методы для сохранения\добавления\обновления БД я вынес в репозиторий UserRepository.
У модели User есть поле $city, которое содержит модель City.
В репозитории UserRepository есть метод findById($id), который ищет пользователя по Id и заполняет модель User.
Проблема в том, что поле $city остаётся не заполненным. Джоинить таблицу `city` прям в методе UserRepository::findById($id) и там создавать модель City? Но это, вроде, не правильно, что UserRepository оперирует моделью City. Как быть в таком случае?
это ок. в ddd есть понятие bounded context (сущность со всеми связями) - репозиторий создается именно для bounded context.
Как будет правильно сделать?

Передавать CityRepositoryInterface ( у которого есть метод ::load($array) который возвращает модель) в UserRepository и с помощью CityRepositoryInterface::load() загружать модель City?
что такое load? что делает внутри? массив (строка из базы) в модель?
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

zelenin писал(а):
Melodic писал(а):
zelenin писал(а): это ок. в ddd есть понятие bounded context (сущность со всеми связями) - репозиторий создается именно для bounded context.
Как будет правильно сделать?

Передавать CityRepositoryInterface ( у которого есть метод ::load($array) который возвращает модель) в UserRepository и с помощью CityRepositoryInterface::load() загружать модель City?
что такое load? что делает внутри? массив (строка из базы) в модель?
load() создаёт модель и наполняет её с помощью set*() данными из $array, $array обычный массив [$column_db=>$value]

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

    public function load( $result)
    {
        $entity = new User();
        $entity->setId($result['id']);
        $entity->setFirstName($result['first_name']);
        $entity->setLastName($result['last_name']);
        $entity->setPatronymic($result['patronymic']);
        $entity->setEmail($result['email']);
        $entity->setPasswordHash($result['password_hash']);
        $entity->setAbout($result['about']);
        $entity->setRegistrationDate(new \DateTime($result['registration_date']));
        $entity->setLastVisitDate(new \DateTime($result['last_visit_date']));
        $entity->setLastVisitIP($result['last_visit_ip']);
        $entity->setRegistrationIP($result['registration_ip']);
        return $entity;
    }
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

тогда я бы посоветовал развязать все это:

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

private function toEntity(array $columns)
{
    return $this->userHydrator->hydrate($columns);
}
 
связанные сущности лучше получать не через другое репо, а тут же - мотивация: другое репо заточено под другой aggregate root и может сразу заполнять сущность связями, которые нужны в одном контексте, но не нужны в контексте первого репозитория. Если не понятно, поясню.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

zelenin писал(а):тогда я бы посоветовал развязать все это:

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

private function toEntity(array $columns)
{
    return $this->userHydrator->hydrate($columns);
}
связанные сущности лучше получать не через другое репо, а тут же - мотивация: другое репо заточено под другой aggregate root и может сразу заполнять сущность связями, которые нужны в одном контексте, но не нужны в контексте первого репозитория. Если не понятно, поясню.
Это всё

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

     $entity = new User();
        $entity->setId($result['id']);
        $entity->setFirstName($result['first_name']);
        $entity->setLastName($result['last_name']);
        $entity->setPatronymic($result['patronymic']);
        $entity->setEmail($result['email']);
        $entity->setPasswordHash($result['password_hash']);
        $entity->setAbout($result['about']);
        $entity->setRegistrationDate(new \DateTime($result['registration_date']));
        $entity->setLastVisitDate(new \DateTime($result['last_visit_date']));
        $entity->setLastVisitIP($result['last_visit_ip']);
        $entity->setRegistrationIP($result['registration_ip']);
        return $entity; 
перенести в UserHydratorInterface::hydrate($array)?

Да, не понятно) если везде использовать lazy load, то таких проблем же не будет возникать?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

Melodic писал(а):
zelenin писал(а):тогда я бы посоветовал развязать все это:

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

private function toEntity(array $columns)
{
    return $this->userHydrator->hydrate($columns);
}
 
связанные сущности лучше получать не через другое репо, а тут же - мотивация: другое репо заточено под другой aggregate root и может сразу заполнять сущность связями, которые нужны в одном контексте, но не нужны в контексте первого репозитория. Если не понятно, поясню.
Это всё

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

     $entity = new User();
        $entity->setId($result['id']);
        $entity->setFirstName($result['first_name']);
        $entity->setLastName($result['last_name']);
        $entity->setPatronymic($result['patronymic']);
        $entity->setEmail($result['email']);
        $entity->setPasswordHash($result['password_hash']);
        $entity->setAbout($result['about']);
        $entity->setRegistrationDate(new \DateTime($result['registration_date']));
        $entity->setLastVisitDate(new \DateTime($result['last_visit_date']));
        $entity->setLastVisitIP($result['last_visit_ip']);
        $entity->setRegistrationIP($result['registration_ip']);
        return $entity;
перенести в UserHydratorInterface::hydrate($array)?
да, ну заодно добавьте метод extract для обратной операции. плюс интерфейс можно сделать общим для всех сущностей.
Melodic писал(а):Да, не понятно) если везде использовать lazy load, то таких проблем же не будет возникать?
ну так мы выяснили, что создать вкусно пахнущий lazy load нетривиальная задача. Ну и невсегда lazy load везде нужен по умолчанию.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

zelenin писал(а):
Melodic писал(а):
zelenin писал(а):тогда я бы посоветовал развязать все это:

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

private function toEntity(array $columns)
{
    return $this->userHydrator->hydrate($columns);
}
связанные сущности лучше получать не через другое репо, а тут же - мотивация: другое репо заточено под другой aggregate root и может сразу заполнять сущность связями, которые нужны в одном контексте, но не нужны в контексте первого репозитория. Если не понятно, поясню.
Это всё

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

     $entity = new User();
        $entity->setId($result['id']);
        $entity->setFirstName($result['first_name']);
        $entity->setLastName($result['last_name']);
        $entity->setPatronymic($result['patronymic']);
        $entity->setEmail($result['email']);
        $entity->setPasswordHash($result['password_hash']);
        $entity->setAbout($result['about']);
        $entity->setRegistrationDate(new \DateTime($result['registration_date']));
        $entity->setLastVisitDate(new \DateTime($result['last_visit_date']));
        $entity->setLastVisitIP($result['last_visit_ip']);
        $entity->setRegistrationIP($result['registration_ip']);
        return $entity; 
перенести в UserHydratorInterface::hydrate($array)?
да, ну заодно добавьте метод extract для обратной операции. плюс интерфейс можно сделать общим для всех сущностей.
Melodic писал(а):Да, не понятно) если везде использовать lazy load, то таких проблем же не будет возникать?
ну так мы выяснили, что создать вкусно пахнущий lazy load нетривиальная задача. Ну и невсегда lazy load везде нужен по умолчанию.
В чём проблема не использовать final?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

Melodic писал(а):В чём проблема не использовать final?
слоеная архитектура подразумевает независимые слои. Не использовать final, означает протекание одного слоя в другой. Нужно найти решение, которое позволит во всех кейсах сделать рабочий lazy. Мои сущности - финальны. Я их помечаю final. То, что другой слой решает lazy через проксирование - это проблема другого слоя.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

zelenin писал(а):
Melodic писал(а):В чём проблема не использовать final?
слоеная архитектура подразумевает независимые слои. Не использовать final, означает протекание одного слоя в другой. Нужно найти решение, которое позволит во всех кейсах сделать рабочий lazy. Мои сущности - финальны. Я их помечаю final. То, что другой слой решает lazy через проксирование - это проблема другого слоя.
Ну и как тогда быть? Делать через $entityRepository->resolve($strategies); ?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

Melodic писал(а):
zelenin писал(а):
Melodic писал(а):В чём проблема не использовать final?
слоеная архитектура подразумевает независимые слои. Не использовать final, означает протекание одного слоя в другой. Нужно найти решение, которое позволит во всех кейсах сделать рабочий lazy. Мои сущности - финальны. Я их помечаю final. То, что другой слой решает lazy через проксирование - это проблема другого слоя.
Ну и как тогда быть? Делать через $entityRepository->resolve($strategies); ?
ничего другого придумать не могу. гугл тоже больше ничего не предлагает.
DomainDependencyResolver::resolve
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

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

Сообщение ElisDN »

Melodic писал(а):Ну и как тогда быть?
Потребность в связях и lazy load обычно возникает при первой же попытке отображать на страницах сами сущности домена. Проблема полностью исчезает, если для листингов и вывода использовать не сами сущности, а отдельные DTO с нужными для каждого листинга наборами полей. В итоге в Repository можно оставить только методы find(), add(), save() и remove(), нужные для непосредственной работы с сущностями, а все остальные для выборок с критериями, паджинациями и сортировками переместить в отдельный ReadRepository.
Закрыто