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

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

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

Сообщение zelenin »

Melodic писал(а):Ещё вопрос есть команда логина LoginCommand.
Есть событие LoginEvent, которое выбрасывается после успешного логина. Так вот, кто должен его бросать? Хендлер LoginHandler или сервис authService (который отвечает за вход и его метод login() вызывается в хендлере)?
интересный вопрос. Думаю, последний сервисный слой - хэндлеры, - как непосредственные исполнители приложения. Но на самом деле тут важен единый подход. Хэндлеры везде обрабатывают реквесты, а сервисы не в каждом хэндлере будут, поэтому надо выбрать тот слой, который будет везде.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

zelenin писал(а): а сервисы не в каждом хэндлере будут, поэтому надо выбрать тот слой, который будет везде.
Т.е. будет везде?)

Ещё вопрос по QueryBus. Она же должна возвращать не саму модель,а её DTO?
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

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

Сообщение slavcodev »

zelenin писал(а):во-первых, перекладываем ответственность за создание модели на фабрику
Разве тут вообще создание модели? Или изменение ее состояния?
Какова ответственность сервиса "UserServiceInterface" или хендлера?
Как решить что пора остановится и количество слоев уже достаточно?
zelenin писал(а):во-вторых, убираем несоответствие в семантике типа changeRole, где на самом деле не смена, а установка роли происходит
Можно ли убрать несоответствие семантики путем переименования метода? Вместо создания дополнительной фабрики?
Разве в фабрике не будет вызван метод changeRole с неверной семантикой? В чем тогда выгода переноса кода из сервиса в новый сервис (чем фабрика является)?
Жду Yii 3!
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

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

Сообщение ElisDN »

Melodic писал(а):Может ли хэндлер напрямую работать с моделью или хенедлер должен вызывать какие то методы сервисов?
Суть перехода к CommandBus в том, что мы код методов бывшего сервиса приложения:

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

class UserService
{
    public function signUp() { ... }
    
    public function updateProfile() { ... }
    
    public function changeRole() { ... }
}
один-в-один разносим по классам:

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

class UserSignUpHandler
{
    public function handle() { ... }
}

class UserProfileUpdateHandler
{
    public function handle() { ... }
}

class UserRoleChangeHandler
{
    public function handle() { ... }
}
А оригинальный UserService удаляем.

Так что как работали с моделями в UserService, так и работаем в хэндлерах.
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

ElisDN писал(а):
Melodic писал(а):Может ли хэндлер напрямую работать с моделью или хенедлер должен вызывать какие то методы сервисов?
Суть перехода к CommandBus в том, что мы код методов бывшего сервиса приложения:

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

class UserService
{
    public function signUp() { ... }
    public function updateProfile() { ... }
    public function changeRole() { ... }
} 
разносим по классам:

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

class UserSignUpHandler
{
    public function handle() { ... }
}

class UserProfileUpdateHandler
{
    public function handle() { ... }
}

class UserRoleChangeHandler
{
    public function handle() { ... }
} 
А оригинальный UserService удаляем.

Так что как работали с моделями в UserService, так и работаем в хэндлерах.
Есть команды регистрации и логина. Логин соответственно логинит, а регистрация и регистрирует и логинит после успешного создания пользователя. Как быть в таком случае, т.к. получается дублирование , если метод логина не вынести в сервис?
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

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

Сообщение ElisDN »

Melodic писал(а):Есть команды регистрации и логина. Логин соответственно логинит, а регистрация и регистрирует и логинит после успешного создания пользователя. Как быть в таком случае, т.к. получается дублирование , если метод логина не вынести в сервис?
Логин:

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

public function handle(UserLoginCommand $command)
{
    $user = $this->userRepository->findByLogin($command->login);
    $this->authManager->login($user);
} 
Регистрация:

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

public function handle(UserSignUpCommand $command)
{
    $user = User::signup($command->login, $command->password, $command->role);
    $this->userRepository->save($user);
    $this->authManager->login($user);
} 
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

Melodic писал(а):
zelenin писал(а): а сервисы не в каждом хэндлере будут, поэтому надо выбрать тот слой, который будет везде.
Т.е. будет везде?)
ну у вас прилетает реквест от клиента (форма, api, консоль) - вы реквест куда-то передаете для обработки. Если вы решили передавать в шину команд, сделав из нее сервисный слой, значит вся соль приложения в этом слое - пусть он и генерит все события.
Melodic писал(а):Ещё вопрос по QueryBus. Она же должна возвращать не саму модель,а её DTO?
шина возвращает модели, отдаете в респонсе дто.

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

$models = $queryBus->execute(new AllPosts);
$dtos = $postCollectionAssembler->toDto($models); 
Аватара пользователя
SiZE
Сообщения: 2817
Зарегистрирован: 2011.09.21, 12:39
Откуда: Perm
Контактная информация:

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

Сообщение SiZE »

У меня вопросец. А если я в сервисе должен сохранить модель, у которой должен быть userId. Хочу понять, как делать правильно?

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

class SubscriptionForm extends \yii\base\Model
{
   private $user;

   public function __construct(IdentityInterface $user, $config = [])
   {
      $this->user = $user;
      parent::__construct($config);
   }

   // rules and other methods...

   public function save()
   {
      $model = new Subscription();
      $model->userId = $this->user->id;
      $service = new SubscriptionService($model);
      $service->create();
   }
}

class SubscriptionService
{
   private $subscription;

   public function __construct(\common\models\Subscription $model)
   {
      $this->subscription = $model;
   }

   public function create()
   {
      if (!$this->subscription->isNewRecord()) {
         throw new InvalidConfigException('Unable to create exist model.');
      }
      return $this->subscription->save(false);
   }
}
С одной стороны у меня есть форма с полями, принимающая данные от пользователя или из другого источника, с другой стороны сервис. Дублировать наборы полей везде не хотелось бы.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

slavcodev писал(а):Разве тут вообще создание модели? Или изменение ее состояния?
а что тут как не создание модели?
начнем с того, что в UL нет понятия "создание класса User". Есть "регистрация юзера", "генерация тестового юзера", "создание юзера через апи"
Допустим, в нашем доменном слое существуют доменные модели (допущение лишь для понимания семантики). Если мы не создаем модель через фабрику, то любое инстанциировнаие черех конструктор нам будет генерить событие UserCreated/UserRegistered, в том числе и создание объекта из данных базы ($userRepo->getById($id)). Решение: создавать объект в фабричных методах, каждый из которых будет генерить свое событие UserRegistered/UserCreatedFromApi/RandomUserGenerated (а инстанциирование в репозитории вообще не будет генерить).
Смотрим дальше: есть бизнес-операция - смена роли из админки. Меняем так:

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

$user->changeRole($role);
Вызов метода генерит событие RoleChanged.

Должна ли регистрация генерить 4 события UserRegistered/RoleChanged/PasswordSetted/LoginSetted? Я считаю, что регистрация - операция атомарная, и событие должно быть одно. Соответственно использование метода changeRole в данном случае неприемлемо. Варианты решения есть - по вкусу разработчика.
slavcodev писал(а):Какова ответственность сервиса "UserServiceInterface" или хендлера?
инкапсулировать всю сопутствующую логику - создание юзера (через фабрику), отсылку писем, логгирование итд.
slavcodev писал(а):Как решить что пора остановится и количество слоев уже достаточно?
Количество слоев не меняется. Речь об одной ответственности на класс. Создание модели может происходить разными способами: через заполнение формы регистрации с 10 полями, реквестом в апи от партнера с тремя полями, генерацией рандомного юзера админом нажатием кнопки Сгенерировать. Три способа создания юзера -> три разных реквеста, три метода в UserFactory (или named constructors - статические фабричные методы), обрабатывающих каждый тип реквеста. А сервис кроме того, что через фабрику создает юзера, еще кидает эвенты в шину, логирует, шлет письма админам, кидает денормализованную модель в elasticsearch.
slavcodev писал(а):
zelenin писал(а):во-вторых, убираем несоответствие в семантике типа changeRole, где на самом деле не смена, а установка роли происходит
Можно ли убрать несоответствие семантики путем переименования метода? Вместо создания дополнительной фабрики?
семантика именно о соответствии именования смыслу.
slavcodev писал(а):Разве в фабрике не будет вызван метод changeRole с неверной семантикой?
выше ответил
slavcodev писал(а):В чем тогда выгода переноса кода из сервиса в новый сервис (чем фабрика является)?
фабрика не является сервисом. Фабрика - класс, знающий о том, как создавать другой класс, полезный при наличии нескольких способов инстанциирования.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

SiZE писал(а):У меня вопросец. А если я в сервисе должен сохранить модель, у которой должен быть userId. Хочу понять, как делать правильно?

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

class SubscriptionForm extends \yii\base\Model
{
   private $user;

   public function __construct(IdentityInterface $user, $config = [])
   {
      $this->user = $user;
      parent::__construct($config);
   }

   // rules and other methods...

   public function save()
   {
      $model = new Subscription();
      $model->userId = $this->user->id;
      $service = new SubscriptionService($model);
      $service->create();
   }
}

class SubscriptionService
{
   private $subscription;

   public function __construct(\common\models\Subscription $model)
   {
      $this->subscription = model$model;
   }

   public function create()
   {
      if (!$this->subscription->isNewRecord()) {
         throw new InvalidConfigException('Unable to create exist model.');
      }
      return $this->subscription->save(false);
   }
} 
С одной стороны у меня есть форма с полями, принимающая данные от пользователя или из другого источника, с другой стороны сервис. Дублировать наборы полей везде не хотелось бы.
непонятно - что где дублировать?
у вас все неверно. точнее save в форме непонятно зачем.

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

$form->load(...);
if(!$form->validate()) {
// обрабатываем ошибки
}
$subscriptionService->createFromForm(SubscriptionForm $form);
 
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

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

Сообщение slavcodev »

zelenin писал(а):а что тут как не создание модели?
начнем с того, что в UL нет понятия "создание класса User". Есть "регистрация юзера", "генерация тестового юзера", "создание юзера через апи"
Вот и я про это. Регистрация пользователя - это процесс изменения модели User в нужное состояние.
Фабрики используются для сложного создании объекта (сложных конструктор: VO, и т.д. в нем)
zelenin писал(а):то любое инстанциировнаие черех конструктор нам будет генерить событие UserCreated/UserRegistered
Это если не правильно использовать конструктор. Конструктор это всего лишь инициализирование модели, никаких событий оно не генерит.
zelenin писал(а):Решение: создавать объект в фабричных методах, каждый из которых будет генерить свое событие UserRegistered/UserCreatedFromApi/RandomUserGenerated (а инстанциирование в репозитории вообще не будет генерить).
Фабрики - создание объектов. Никаких событий не должны генерить. По хорошему их генерят рич-модели, на крайний случай сервисы (хотя это нарушает инкапсуляцию модели)
zelenin писал(а):Смотрим дальше: есть бизнес-операция - смена роли из админки. Меняем так:

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

$user->changeRole($role); 
Вызов метода генерит событие RoleChanged.

Должна ли регистрация генерить 4 события UserRegistered/RoleChanged/PasswordSetted/LoginSetted? Я считаю, что регистрация - операция атомарная, и событие должно быть одно. Соответственно использование метода changeRole в данном случае неприемлемо. Варианты решения есть - по вкусу разработчика.
Вот поэтому я рекомендую инкапсулировать все в рич-модели

Регистрация и соотв. события

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

$user->register(...); 
Смена роли (именно смены как юз кейс) и соотв. события

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

$user->changeRole($role); 
В обоих поведениях модели будет использоваться (приватный метод setRole)
zelenin писал(а):Я считаю, что регистрация - операция атомарная, и событие должно быть одно.
Тут все зависит от бизнеса.
Доменное событие - событие произошедшее с агрегейтом важное для бизнеса.
Т.е. вполне возможно что для бизнеса важно записать не одно событие при регистрации.
zelenin писал(а):
slavcodev писал(а):Какова ответственность сервиса "UserServiceInterface" или хендлера?
инкапсулировать всю сопутствующую логику - создание юзера (через фабрику), отсылку писем, логгирование итд.
А хендлер для команды тогда чем занимается если не тем же что ты описал для сервиса?
zelenin писал(а):Три способа создания юзера -> три разных реквеста, три метода в UserFactory (или named constructors - статические фабричные методы), обрабатывающих каждый тип реквеста. А сервис кроме того, что через фабрику создает юзера, еще кидает эвенты в шину, логирует, шлет письма админам, кидает денормализованную модель в elasticsearch.
Так и почему это не может сделать эти три хендлеры? Каждый свою логику содержит, свою ответственность? Зачем новый слой - сервисы?
zelenin писал(а):фабрика не является сервисом. Фабрика - класс, знающий о том, как создавать другой класс, полезный при наличии нескольких способов инстанциирования.
ОК. Тут дело в том что "сервисы" понятие используемое в разных контекстах. Есть "Сервисы" - юз кейсы", про то что ты говоришь, и "сервисы" в контексте деления классов на два типа "Data-objects" и "Services objects", эти я имел виду, фабрика это объект-сервис.
Жду Yii 3!
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

slavcodev писал(а):Вот и я про это. Регистрация пользователя - это процесс изменения модели User в нужное состояние.
а как надо изменить объект, чтобы зарегистрировать юзера?
slavcodev писал(а):Фабрики используются для сложного создании объекта (сложных конструктор: VO, и т.д. в нем)
фабрики используются для создания объекта (я кстати указал варианты использования фабрик - разные методы на разные реквесты)
slavcodev писал(а):
zelenin писал(а):то любое инстанциировнаие черех конструктор нам будет генерить событие UserCreated/UserRegistered
Это если не правильно использовать конструктор.
а как надо правильно?
slavcodev писал(а):Конструктор это всего лишь инициализирование модели, никаких событий оно не генерит.
а что тогда генерит?
slavcodev писал(а):Фабрики - создание объектов. Никаких событий не должны генерить. По хорошему их генерят рич-модели, на крайний случай сервисы (хотя это нарушает инкапсуляцию модели)
модель и будет генерить

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

$model->raise(new UserRegistered); 
только в зависимости от фабрики разные события будут.
slavcodev писал(а):
zelenin писал(а):Смотрим дальше: есть бизнес-операция - смена роли из админки. Меняем так:

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

$user->changeRole($role);
Вызов метода генерит событие RoleChanged.

Должна ли регистрация генерить 4 события UserRegistered/RoleChanged/PasswordSetted/LoginSetted? Я считаю, что регистрация - операция атомарная, и событие должно быть одно. Соответственно использование метода changeRole в данном случае неприемлемо. Варианты решения есть - по вкусу разработчика.
Вот поэтому я рекомендую инкапсулировать все в рич-модели
да все верно. Но регистрация юзера - это именно создание объекта.
slavcodev писал(а):Регистрация и соотв. события

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

$user->register(...);
что тут метод register делает? зачем он на готовом объекте? мне кажется ты пошел другим путем - регистрация после создания объекта, а не создание объекта как регистрация. Интересно, но распиши тогда метод register.
slavcodev писал(а):В обоих поведениях модели будет использоваться (приватный метод setRole)
zelenin писал(а):Я считаю, что регистрация - операция атомарная, и событие должно быть одно.
Тут все зависит от бизнеса.
Доменное событие - событие произошедшее с агрегейтом важное для бизнеса.
Т.е. вполне возможно что для бизнеса важно записать не одно событие при регистрации.
да ради бога. Если для целей бизнеса нужно чтобы кроме UserChangedFromApi генерилось RoleChanged, сделаем. Но мы не про цели бизнеса, а про конкретное бизнес-событие - "юзер зарегистрирован". Без доп. запросов от бизнеса событие одно. Не будешь же ты на все кейсы генерить все возможные события.
slavcodev писал(а):
zelenin писал(а):
slavcodev писал(а):Какова ответственность сервиса "UserServiceInterface" или хендлера?
инкапсулировать всю сопутствующую логику - создание юзера (через фабрику), отсылку писем, логгирование итд.
А хендлер для команды тогда чем занимается если не тем же что ты описал для сервиса?
хэндлер команды и есть сервис. Ты либо используешь сервисный слой либо шину команд.
slavcodev писал(а):
zelenin писал(а):Три способа создания юзера -> три разных реквеста, три метода в UserFactory (или named constructors - статические фабричные методы), обрабатывающих каждый тип реквеста. А сервис кроме того, что через фабрику создает юзера, еще кидает эвенты в шину, логирует, шлет письма админам, кидает денормализованную модель в elasticsearch.
Так и почему это не может сделать эти три хендлеры? Каждый свою логику содержит, свою ответственность? Зачем новый слой - сервисы?
и тут я не понял о чем ты. Я нигде не писал про параллельно существующие хэндлеры и сервисы. Сервисы могут использоваться внутри хэндлеров, но уже никак сервисы сервисного слоя, а как сервис какой-то сторонней сущности - сервис авторизации от сторонней либы, сервис-клиент апи итд. Сервис - это всего лишь служба, а не обязательно часть сервисного слоя.
slavcodev писал(а):
zelenin писал(а):фабрика не является сервисом. Фабрика - класс, знающий о том, как создавать другой класс, полезный при наличии нескольких способов инстанциирования.
ОК. Тут дело в том что "сервисы" понятие используемое в разных контекстах. Есть "Сервисы" - юз кейсы", про то что ты говоришь, и "сервисы" в контексте деления классов на два типа "Data-objects" и "Services objects", эти я имел виду, фабрика это объект-сервис.
ну вот, о чем я выше написал. Сервис - это практически все что угодно, выполняющее какую-либо служебную задачу. Условно и фабрику можно назвать сервисом, но обычно фабрика есть фабрика.
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

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

Сообщение slavcodev »

zelenin писал(а):а как надо изменить объект, чтобы зарегистрировать юзера?
Это доменный эксперт может рассказать. Предполагаю что нужно ввести: имя и другие поля, установить в модели время регистрации, бросить события нужные.
zelenin писал(а):фабрики используются для создания объекта (я кстати указал варианты использования фабрик - разные методы на разные реквесты)
zelenin писал(а):Есть "регистрация юзера", "генерация тестового юзера", "создание юзера через апи"
Я считаю это не варианты использования фабрик, все это сценарии использования (Use cases), разные сервисы,
которые могут вызывать один и больше поведения модели, тем самым изменяя ее состоянии.
Но вызывать поведения уже созданной модели.
zelenin писал(а):а как надо правильно?
zelenin писал(а):а что тогда генерит?
Хочу заметить что когда я говорю "правильно", это мое личное мнение, полученное моим персональным опытом работы.
Я не кого не пытаюсь научить, и не пытаюсь быть умнее остальных.


Я считаю, что конструктор, это всего лишь элемент ООП, инициализирование модели в ее начально но валидное состояние. Считая черновик, полотно для последующих изменений.
На примере пользователя и тех трех юз кейсов, предположу что модель имеет следующие свойства, конструктор и поведения (с пояснениями в комментах)

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

class User
{
  private $email;
  private $name;
  private $password;
  private $createdTime;
  private $registeredTime;
  private $status;
  private $confirmationToken;
  private $role;
  
  /**
    * Чтоб инициализировать модель в ее начальное валидное состояние, конструктор должен:
    * - Требовать значение умолчания которых нельзя выдумать
    * - Установить свойствам модели умолчания
    *
    * Любая сущность имеет идентификатор (ИД) отличающий ее от других.
    * Идентификатор это не обязательно UUID, или инкрементное число, это уникальное значение в контексте этой модели.
    * Я допускаю что наш домен требует емайл пользователя, и он достаточно уникален, это и будет ИД сущности,
    * его мы запрашиваем. Начальный статус "inactive". Для остальных свойств "null" является валидным значением.
    * 
    * Никаких событий конструктор не бросает, т.к. для домена никакого интереса это не вызывает.
    */
  function __constructor($email)
  {
    $this->email = $email;
    $this->status = 'inactive';
    $this->role = 'User';
  }
  
  /**
    * Поведение пользователя, переход в состояние тестового пользователя.
    *
    * По условиям домена пароль может придумывает админ, или каким-то сервисом генерится.
    * Токен генерится стороним сервисом.
    *
    * Устанавливаем нужный статус и время создания пользователя (не путать со временем создания объекта)
    */
  function draft($password, $confirmationToken)
  {
    if ($this->createdTime !== null) {
      throw new LogicException('Лажа в архитектуре, попытка создать тестового пользователя второй раз');
    }
  
    $this->password = $password;
    $this->confirmationToken = $confirmationToken;
    $this->createdTime = new DateTime();
    $this->status = 'pending-confirmation';
    
    $this->record(new TestUserWasCreatedEvent());
  }
  
  /**
    * Поведение пользователя, переход в состояние только что зарегистрированного пользователя.
    * 
    * Требуем необходимые от пользователя данные.
    * Устанавливаем нужный статус, время создания пользователя (не путать со временем создания объекта)
    * и время регистрации (допуская что это требование домена, может и не быть)
    */
  function register($name, $password, $confirmationToken)
  {
    if ($this->createdTime !== null) {
      throw new LogicException('Лажа в архитектуре, попытка зарегистрировать пользователя второй раз');
    }
  
    $this->name = $name;
    $this->password = $password;
    $this->confirmationToken = $confirmationToken;
    $this->createdTime = new DateTime();
    $this->registerTime = new DateTime();
    $this->status = 'pending-confirmation';
    
    $this->record(new NewUserWasRegisteredEvent());
  }
  
  /**
    * Поведение пользователя, активация (подтверждение).
    */
  function activate($confirmationToken)
  {
    if ($this->status === 'active') {
      throw new LogicException('Лажа в архитектуре, попытка активировать пользователя второй раз');
    }
  
    if ($this->confirmationToken !== $confirmationToken) {
      throw new UnexpectedValueException('Ай-ай-ай, код не подходит');
    }
  
    // при условии что домен ставит такое требование, др. вариант просто установить время регистрации в момент активации.
    if ($this->registerTime === null) {
      throw new DomainException('Пользователь с тестовым паролем и без имени, нужно перенаправить и запросить новый');
    }
  
    $this->status = 'active';
    
    $this->record(new UserWasActivatedEvent());
  }
  
  /**
    * Поведение пользователя, активация (подтверждение) тестового пользователя,
    * с обязательной сменой пароля и имени по умолчанию.
    *
    * Здесь два события будут брошены, т.к. допускаем что домену очень важно знать и среагировать на оба.
    */
  function acceptInvitation($confirmationToken, $name, $password)
  {
    if ($this->status === 'active') {
      throw new LogicException('Лажа в архитектуре, попытка активировать пользователя второй раз');
    }
  
    if ($this->confirmationToken !== $confirmationToken) {
      throw new UnexpectedValueException('Ай-ай-ай, код не подходит');
    }
  
    $this->name = $name;
    $this->password = $password;
    $this->registerTime = new DateTime();
    $this->status = 'active';
    
    $this->record(new UserInvitationWasAcceptedEvent());
    $this->record(new UserWasActivatedEvent());
  }
  
  /**
    */
  function changeRole(User $admin, $role)
  {
    if ($admin->role !== 'Admin') {
      throw new LogicException('Лажа в архитектуре, попытка поменять роль, не имея нужных админских прав');
    }
  
    $this->role = $role;
    
    $this->record(new UserRoleWasChangedEvent());
  }
}
Валидация входных данных упущена, предположим что она происходит в сервисах.
Дубликаты кода в некоторых методах, рефакторингом убираются в приватные методы модели.

Сервисы для юз кейсов (при помощи хендлеров для команд, хотя и без них если не хочется CQRS)

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

class CreateTestUserHandler
{
  /**
    * Емайл вводит админ в админке.
    *
    * passwordGenerator, tokenGenerator, userRepository - сервисы зависимости хендлера, устанавливаются в конструтокре.
    */
  function handle(CreateTestUserCommand $data)
  {
    $user = new User($data->email);
    $user->draft($this->passwordGenerator->generate(), $this->tokenGenerator->generate());
    
    $this->userRepository->save($user);
  }
}

class RegisterUserHandler
{
  /**
    * Емайл, имя, пароль вводит пользователь в форме.
    * Подтверждающий пароль упущен, я считаю это не имеет смысла для домена,
    * а только часть UI, чтоб пользователь не ошибся, так что это оставляем на ответсвенность валидации команды.
    * 
    * tokenGenerator, userRepository - сервисы зависимости хендлера, устанавливаются в конструтокре.
    */
  function handle(RegisterUserCommand $data)
  {
    $user = new User($data->email);
    $user->register(
      $data->name,
      $data->password,
      $this->tokenGenerator->generate()
    );
    
    $this->userRepository->save($user);
  }
}

class CreateUserApiHandler
{
  /**
    * Не могу придумать отличие от регистрации через форму.
    * Для домена, для сущности пользователя, вроде как все равно из какого приложения регистрируется пользователь, хоть из консоли.
    * От балды могу допустить что через апи можно сразу активировать указав token (или может другой атрибут)
    */
  function handle(CreateUserApiCommand $data)
  {
    $user = new User($data->email);
    $user->register(
      $data->name,
      $data->password,
      $data->confirmationToken ?? $this->tokenGenerator->generate()
    );
    
    if ($data->confirmationToken !== null) {
      $user->activate($data->confirmationToken);
    }
    
    $this->userRepository->save($user);
  }
}

class ActivateUserHandler
{
  /**
    * 1. getByEmail - всегда возвращает сущность или бросает исключение
    * 2. Ловить исключения тут можно если нужно, но это может делать контролер,
    *     который ответит либо 404 либо перенаправит на страницу где нужно ввести имя и пароль для тестового пользователя
    */
  function handle(ActivateUserCommand $data)
  {
    $user = $this->userRepository->getByEmail($data->email);
    
    $user->activate($data->confirmationToken);
    
    $this->userRepository->save($user);
  }
}

class ActivateTestUserHandler
{
  /**
    */
  function handle(ActivateTestUserCommand $data)
  {
    $user = $this->userRepository->getByEmail($data->email);
    
    $user->activate($data->confirmationToken, $data->name, $data->password);
    
    $this->userRepository->save($user);
  }
}

class UserController
{
  function activateAction()
  {
     $email = $this->httpRequest->getQuery('email');
     $token = $this->httpRequest->getQuery('token');
  
     try {
       $this->commandBus->dispatch(new ActivateUserCommand($email, $token))
     } catch (OutOfRangeException $e) {
       return $this->notFound('Пользователь не найден');
     } catch (DomainException $e) {
       return $this->redirect('activateTestUser', compact('email', 'token'));
     }     
     
     return $this->redirect('login');
  }

  function activateTestUserAction()
  {
     $email = $this->httpRequest->getQuery('email');
     $token = $this->httpRequest->getQuery('token');
     $name = $this->httpRequest->getPost('name');
     $password = $this->httpRequest->getPost('password');
  
     try {
       $this->commandBus->dispatch(new ActivateTestUserCommand($email, $token, $name, $password))
     } catch (OutOfRangeException $e) {
       return $this->notFound('Пользователь не найден');
     }
     
     return $this->redirect('login');
  }
}

class UserRepository
{
  function save(User $user)
  {
    // сохраняем пользователя в базу данных (имплементация упущена)
    // после чего обрабатываем события произошедшие с ним,
    // отсылаем емитеру чтоб оповестил подписчиков из других контектов
    foreach ($user->flushEvents() as $event) {
      $this->eventEmitter->emit($event);
    }
  }
}
zelenin писал(а):модель и будет генерить

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

$model->raise(new UserRegistered); 
только в зависимости от фабрики разные события будут.
Не понял что ты имел виду, по коду который ты привел, событие генерит сервис (фабрика) и передает в модель.
Это нарушает инкапсуляцию модели, можно забыть ошибиться с событием в общем на лажать.
Да и ненужно фабрике или другим сервисам знать о бизнес правилах домена, частью чего являются события.
zelenin писал(а):и тут я не понял о чем ты. Я нигде не писал про параллельно существующие хэндлеры и сервисы
Да, я не правильно растолковал твои сообщения, перечитал, понял свою ошибку и верно.
zelenin писал(а):Фабрика - класс, знающий о том, как создавать другой класс, полезный при наличии нескольких способов инстанциирования.
Угу, только иницилизирование сущности по сути два: новая сущность не существующая в базе в состоянии начальном,
и сущность восстановленная из хранилища, в состоянии сохраненным. Вот это я называю "инстанциирования".
Все остальное: регистарция, активация, изменение роли, пароля, это поведения объекта, изменяющее его состояние.
Жду Yii 3!
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

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

Сообщение zelenin »

вообще в целом мне понравился твой подход, но он не традиционен. Ты по сути в конструктор подаешь только идентификатор, а три предложенных мною статических фабричных метода (так называемые именованные конструкторы (с) Верраес) переносишь в поведенческие методы объекта, делая more ddd. Пожалуй, возьму на вооружение.
zelenin писал(а):модель и будет генерить

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

$model->raise(new UserRegistered);
только в зависимости от фабрики разные события будут.
Не понял что ты имел виду, по коду который ты привел, событие генерит сервис (фабрика) и передает в модель.
Давай не будем фабрику называть сервисом, чтобы не путать с сервисами сервисного слоя и инфраструктурными сервисами. Фабрика - это фабрика.
Это нарушает инкапсуляцию модели, можно забыть ошибиться с событием в общем на лажать.
поэтому мы создаем одну точку входа - UserFactory::registerUseCase(..)
Да и ненужно фабрике или другим сервисам знать о бизнес правилах домена, частью чего являются события.
фабрика - часть слоя, в которому создает объект - домена. Но в целом ты прав - ей не следует знать о событии. То, что предложил я, был вариант обойти то, что модель может быть создана несколькими способами, должна выкинуть разные события, а конструктор у нас один. Но твой вариант с $user->register(..) мне понравился.

Резюмирую: создание объекта на основе одного id - вещь нетрадиционная и мной ни разу не встречаемая у известных авторов, но предложенное тобой решение, имхо, красивое с т.з. ddd.
Аватара пользователя
SiZE
Сообщения: 2817
Зарегистрирован: 2011.09.21, 12:39
Откуда: Perm
Контактная информация:

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

Сообщение SiZE »

zelenin писал(а):

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

$subscriptionService->createFromForm(SubscriptionForm $form);
А модель Subscription в сервис как прокинуть? И может ли сервис работать с несколькими наследниками модели Subscription? И где присваивать userId? =)
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

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

Сообщение slavcodev »

zelenin писал(а):статических фабричных метода (так называемые именованные конструкторы (с) Верраес)
Я сразу понял про что ты, я сам читаю статьи Mathias.
Но к этому подходу отношусь неуверенно. Я считаю что оно нарушает принципы ООП, а именно инкпасуляцию,
из-за того что устанавливаются приватные свойства.
До того момента когда он делал конструтор приватный и без параметров.

ИМХО это просто более современный хак подхода с рефлексией, используемый некоторыми ОРМ.
zelenin писал(а):Давай не будем фабрику называть сервисом, чтобы не путать с сервисами сервисного слоя и инфраструктурными сервисами. Фабрика - это фабрика.
Фабрика это один из типов порождающих паттернов. Есть и другие варианты.
Применимые для проектирования сервисов создающих объекты.

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

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

Это я уточнил, т.к. сообщением чуть выше, кто-то писал сервис, передавай данный в конструтор.

А Infrastructure services, Application services, Domain services - все это уровни в которых используются эти объекты и какие данными обрабатывают.

Сервисы сервисного слоя - это совсем из другой оперы. Это элементы Union Architecture (слоистой архитектуры, что-то типа улучшение MVC), где между C и M, добавляется еще один слой. Эти сервисы по идее могут быть любого из тех трех типов что я выше написал.
zelenin писал(а):создание объекта на основе одного id - вещь нетрадиционная и мной ни разу не встречаемая у известных авторов
Небольшое уточнение:

Не одного ИД, а на основе данных без которых объект не может принять свое начальное состояние.

В других сущностях это может быть не один ИД, даже с User это может быть, все зависит от домена.

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

В случае с Value Object, т.к. это модель комплексного значения, состоящего из нескольких данных, скорее всего все они являются обязательными для инициализации и должны сразу устанавливаться в конструтор.
Жду Yii 3!
Melodic
Сообщения: 87
Зарегистрирован: 2016.05.11, 17:43
Откуда: Луганск

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

Сообщение Melodic »

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

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

Сообщение ElisDN »

slavcodev писал(а):Я считаю, что конструктор, это всего лишь элемент ООП, инициализирование модели в ее начально но валидное состояние. Считая черновик, полотно для последующих изменений.
Ваш подход можно и со статическими конструкторами совместить:

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

class User
{
  ...
  
  function __constructor($email)
  {
    $this->email = $email;
    $this->status = 'inactive';
    $this->role = 'User';
  }
    
  static function draft($email, $password, $confirmationToken)
  { 
    $user = new self($email);
    $user->password = $password;
    $user->confirmationToken = $confirmationToken;
    $user->createdTime = new DateTime();
    $user->status = 'pending-confirmation';
    $user->recordEvent(new TestUserWasCreatedEvent($user));
    return $user;
  }
  
  static function register($email, $name, $password, $confirmationToken)
  {
    $user = new self($email);
    $user->name = $name;
    $user->password = $password;
    $user->confirmationToken = $confirmationToken;
    $user->createdTime = new DateTime();
    $user->registerTime = new DateTime();
    $user->status = 'pending-confirmation';
    $user->recordEvent(new NewUserWasRegisteredEvent($this));
    return $user;
  }

  function activate($confirmationToken)
  {
    $this->guardIsActive();
    $this->guardConfirmationTokenIsCorrect($confirmationToken);  
    $this->status = 'active';    
    $this->recordEvent(new UserWasActivatedEvent($this));
  }
} 
без необходимости вызова new User($email) в Use Case:

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

function handle(RegisterUserCommand $data)
{
  $user = User::register($data->email, $data->name, $data->password, $this->tokenGenerator->generate());
  $this->userRepository->save($user);
} 
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

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

Сообщение slavcodev »

ElisDN писал(а):Ваш подход можно и со статическими конструкторами совместить
Возможно конечно, но я предпочитаю так не делать.
Причину написал выше, лично я считаю это обходом инкапсуляции через установку значений приватным свойствам не через конструктор или поведения сущности.
Жду Yii 3!
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

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

Сообщение ElisDN »

Melodic писал(а):Возможно ли это всё в какой то степени применить к ActiveRecord, как к доменной модели?
Возможно, только не сможете переписать оригинальный конструктор. И вложенные объекты вроде $company->address->name нужно будет реализовывать через связи hasOne.
Закрыто