Разделение агрегата

Обсуждаем, как правильно строить приложения
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

Re: Разделение агрегата

Сообщение Bio man »

Вроде понял. Domain вызывается из App, App вызывается из Presentation.

Как то так:

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

namespace App\Domain\Staff\Services;

interface AuthenticationService
{
    public function authenticate(string $token);

    public function login(Email $email, Password $password, \DateTimeImmutable $expirationTime);

    public function logout(string $token);
}
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

Re: Разделение агрегата

Сообщение noLogicOnlyWar »

В теории да, ведь инфраструктура знает о домене, но передачи дто будет нарушить ubiquitous language в терминах которого спроектирован доменный сервис. Думаю так.
В любом случае что мешает сохранить цепочку инфраструктура -> app сервис -> доменный сервис? App сервис у нас будет конкретный сценарий приложения - LoginService, а доменный сервис уже действие в терминах языка - AuthManger->login($user, $tokenGenerator)
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

Re: Разделение агрегата

Сообщение Bio man »

Если я не хочу намертво привязываться к аутентификации по токену, то в сервис аутентификации я могу передавать самого юзера.

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

interface AuthenticationServiceInterface
{
    public function authenticate(StaffMember $staffMember);

    public function login(Email $email, Password $password, \DateTimeImmutable $expirationTime);

    public function logout(StaffMember $staffMember);
}
В app service буду получать юзера каким то образом, например по токену (или этим должен заниматься инфраструктурный сервис?) и передавать его доменному сервису AuthenticationService.

Но я хочу управлять входами юзеров из админки.

Для этого мне нужно создать некую абстракцию вместо токена?
Например, AuthToken теперь будет AuthSession.

AuthSession::$id будет хранить либо токен, либо ID сессии, либо еще что-то, что будет идентифицировать сессию.

Нормальный вообще такой подход?
Или можно как-то иначе сделать?
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

Re: Разделение агрегата

Сообщение Bio man »

Примерно так выглядел бы агрегат сессии аутентификации

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

class AuthenticationSession implements AggregationRootInterface
{
    private $id;
    private $staffMember;
    private $creationTime;
    private $expirationTime;
    private $lastActivityTime;
    private $userAgent;
    private $ip;

    ....
}
Вопрос. На какой слой возложить ответственность за реализацию способа аутентификации?
Если на App layer, то для каждого способа нужны будут разные сервисы.
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

Re: Разделение агрегата

Сообщение noLogicOnlyWar »

В app service буду получать юзера каким то образом, например по токену (или этим должен заниматься инфраструктурный сервис?) и передавать его доменному сервису AuthenticationService.
Можно заинжектить в app сервис репозитроий

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

class LoginCommand 
{
	public function execute ($dto)
	{
		$member = $this->getMemberOrFail($dto->userName);
		$this->authManager->login($member, ...);
	}
	
	private function getMemberOrFail($name)
	{
		if ( ($member = $this->memberRepository->ofName($name)) === null){
			throw new MemberNotFoundException();
		}
		return $member;
	}
}
Для этого мне нужно создать некую абстракцию вместо токена?
Если есть такая потребность то нужна абстракция, согласен.
Вопрос. На какой слой возложить ответственность за реализацию способа аутентификации?
Интерфейс в домене а реализация в инфраструктурном слое, тк домен ничего не знает о сессиях, а токен вы вообще можете генерить через сторонние либы (например если jwt токен нужен).
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

Re: Разделение агрегата

Сообщение Bio man »

Картина начинает проясняться.
Но остался открыт вопрос. В сервис аутентификации лучше передавать юзера или сессию?

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

public function authenticate(StaffMember $staffMember);
// или
public function authenticate(AuthSession $authSession);
С точки зрения реализации видится одинаково.
Что мы сперва получим пользователя по данным из сессии, что получим сессию по тем-же данным. Правда в 1 случае придется сделать 2 запроса к репозиторию (сперва ссессию, потом юзера получить), во 2 1 запрос (но 2 запрос уже будет в самом сервисе).

И должен ли агрегат юзера содержать в себе свои сессии? Или сессия должна содержать юзера?
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

Re: Разделение агрегата

Сообщение noLogicOnlyWar »

Лучше юзера, тк login($user) логично и соответствует терминам предметной области.
И должен ли агрегат юзера содержать в себе свои сессии? Или сессия должна содержать юзера?
Я за 2й вариант, невижу причин юзеру знать о существование сессии (кстати нужно ли знать домену о существование сессии? видимо нет, ведь это конкретное хранилище некоторых данных). Для сессии вообще лучше взять что-то что реализует psr7 session (хотя не суть, всеравно адаптер писать), поместить это что-то в инфраструктуру а в домене будет абстрактное хранилище для AuthSession ну и написать адаптер либы к интерфейсу нашего хранилища. Только в AuthenticationSession::$staffMember надо $staffMemberId хранить, если это в сессии будет лежать.
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

Re: Разделение агрегата

Сообщение Bio man »

noLogicOnlyWar писал(а): 2017.12.25, 22:47 Лучше юзера, тк login($user) логично и соответствует терминам предметной области.
А вот мне теперь кажется, что передавать нужно сессию (абстракцию).
Сессия это по сути identity юзера.
И в методе authenticate доменного сервиса сам юзер не нужен по сути.
Этот метод обновит данные сессии (время аутентификации, IP итд), либо кинет исключение, что сессия не валидна (истек срок, например).
Иначе, как метод authenticate определит по какой сессии аутентифицировать юзера?

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

public function authenticate(AuthenticationSession $authenticationSession);
noLogicOnlyWar писал(а): 2017.12.25, 22:47 (кстати нужно ли знать домену о существование сессии? видимо нет, ведь это конкретное хранилище некоторых данных).
Я имел в виду абстракцию AuthSession. Разумеется, об инфраструктуре домен не знает.
noLogicOnlyWar писал(а): 2017.12.25, 22:47 Только в AuthenticationSession::$staffMember надо $staffMemberId хранить
тоже думал об этом, согласен, незачем усложнять.

Набросал черновик, что думаешь?

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

namespace Domain\Services;

class AuthenticationService
{
    public function authenticate(AuthenticationSession $authenticationSession) { ... }
	
	...
}

//=====================================================

namespace Application\Services;

interface AuthenticationServiceInterface
{
	public function authenticate(): void;
	
	...
}

//=====================================================

namespace Infrastructure\Services;

class SessionAuthenticationService implements AuthenticationServiceInterface
{
	public function __construct(YiiSession $session, AuthSessionRepository $repo, AuthenticationService $authService) { ... }
	
    public function authenticate()
	{
		$sessionId = $this->session->id;
		$authSession = $this->repo->get($sessionId);
		
		$this->authService->authenticate($authSession);
		
		$this->repo->save($authSession);
	}
	
	...
}
Тут меня больше интересует то, что в app layer лежит интерфейс, а реализуется он в infrastructure, это правильный подход?
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

Re: Разделение агрегата

Сообщение noLogicOnlyWar »

Ну да, как описал authenticate принимает $authSession, согласен, но login то user'a принимает.
Тут меня больше интересует то, что в app layer лежит интерфейс, а реализуется он в infrastructure, это правильный подход?
В твоем примере помойму напутанно:
1) SessionAuthenticationService в примере по сути сервис приложения и должен находиться там (тк выполняет координационную логику), то есть код из него я бы переместил в Application\AuthenticationService

2) AuthenticationServiceInterface этот интерфейс как раз должен быть в домене
3) ну а SessionAuthenticationService являлся бы реализацией AuthenticationServiceInterface из домена.
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

Re: Разделение агрегата

Сообщение Bio man »

noLogicOnlyWar писал(а): 2017.12.26, 00:19 но login то user'a принимает.
login принимает email и пароль.

1) SessionAuthenticationService это инфраструктура, т.к. взаимодействует с йишными штуками. И в то же время это app, т.к. реализует интерфейс из app.
2) Почему? Я доменный аутентификатор вызываю в app аутентификаторе.
По моему должно быть 2 сервиса: доменный инкапсулирует БЛ, сервис приложения разруливает, т.е. получает агрегат сессии, передает домену и сохраняет.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Разделение агрегата

Сообщение sda »

Bio man писал(а): 2017.12.25, 21:42 должен ли агрегат юзера содержать в себе свои сессии? Или сессия должна содержать юзера?
Должен или нет определяется наличием инвариантов. Если инварианты есть тогда сущность является частью агрегата, в противном случае нет. Это правило можно найти в книге в Implementing domain-driven design. Вообще там сказано, что агрегат используется исключительно для соблюдения инвариантов. Ни для чего более.

if ($this->isArchived()) {
throw new AuthenticationFailedException('Archived staff members can not be logged in.');
}

if (isset($this->authenticationTokens[$token->getToken()])) {
throw new AlreadyLoggedInException();
}
Вы каждое исключение в доменном слое обрабатываете по разному ? Просто интересно для какой цели вы заводите по классу на исключение.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Разделение агрегата

Сообщение sda »

А вообще я не понимаю, зачем здесь доменный сервис. Задача решается обычным application сервисом. Например так.

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

class AuthenticationService {
  public function __construct(
    TokenRepository $tokenRepository,
    UserRepository $userRepository,
    Security $security
  ) { 
    $this->tokenRepository = $tokenRepository;
    ...
  }
  public function authenticate($id) {
    $token = $this->tokenRepository->findById($id);
    
    if ($token === null) {
      throw new NotFoundHttpException('Token not found.');
    }
    
    $token->increaseExpirationTime();
    $this->tokenRepository->save($token);
    ...
  }
  public function login($email, $password) {
    $user = $this->userRepository->findByEmail($email);
    
    if ($user === null || $user->passwordHash !== $this->security->hash($password)) {
      throw new NotFoundHttpException('Invalid email or password.');
    }
    
    $token = new Token(
      new TokenId(),
      $user->id,
      ...
    );
    
    $this->tokenRepository->save($token);
    ...
  }
  public function logout($id) {
    $token = $this->tokenRepository->findById($id);
    
    ...
    $this->tokenRepository->remove($token);
    ...
  }
}
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

Re: Разделение агрегата

Сообщение Bio man »

sda писал(а): 2017.12.26, 01:38 Вы каждое исключение в доменном слое обрабатываете по разному ? Просто интересно для какой цели вы заводите по классу на исключение.
Можно на ты. Для того, что бы знать, что именно пошло не так. Все доменные исключения наследуют \DomainException.

В app service не хочу помещать БЛ валидации токена, например, проверку срока годности или проверку статуса юзера.
Поэтому доменный сервис, что бы эту логику инкапсулировать.
А в app уже разруливаю, что куда...
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Разделение агрегата

Сообщение sda »

Bio man писал(а): 2017.12.26, 03:11 В app service не хочу помещать БЛ валидации токена, например, проверку срока годности или проверку статуса юзера.
Ну проверка срока годности укладывается в Token::increaseExpirationTime(). Я бы всё же поискал решение, чтобы обойтись без доменного сервиса и бизнес-логику токена поместить собственно в токен.
Bio man писал(а): 2017.12.26, 03:11 Для того, что бы знать, что именно пошло не так.
Думаю начнется лавинообразный рост файлов исключений. Что пошло не так можно понять и из сообщения. Необязательно для этого создавать новый тип исключения. Можно посмотреть как сделано у других
Не подумайте, что умничаю. Просто для размышлений. Всё таки первый репозиторий от Вернона, а второй сам Эванс одобрил.
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

Re: Разделение агрегата

Сообщение noLogicOnlyWar »

sda писал(а): 2017.12.26, 02:14 А вообще я не понимаю, зачем здесь доменный сервис. Задача решается обычным application сервисом. Например так.
А почему вы против доменного сервиса? Допустим у меня после логина должно выбрасываться доменный евент, как быть?
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Разделение агрегата

Сообщение sda »

noLogicOnlyWar, я не против. Вернон пишет, что доменные сервисы необходимо использовать, когда бизнес-логика не принадлежит никакой сущности, но тем не менее где-то должна быть и отмечает, что такое происходит крайне редко и в большинстве случаев все же можно найти необходимую сущность для бизнес-логики. Также он обращает внимание, что когда программист начинает неоправданно использовать доменные сервисы это ведет к анемичной модели. Он указывает на использование доменных сервисов как на исключение из общих правил. Когда других вариантов нет. Соответственно их должно быть минимальное количество. Лучше если их нет вообще.
noLogicOnlyWar писал(а): 2017.12.26, 10:39 Допустим у меня после логина должно выбрасываться доменный евент, как быть?
Можно опять же посмотреть как у других. Вариации встречаются разные, кто-то против использования статики в доменном объекте и вместо публикации копят события, а публикацию делают кто в application сервисах, кто в репозиториях. Дело выбора. Но все рождают доменное событие внутри доменного объекта.
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

Re: Разделение агрегата

Сообщение noLogicOnlyWar »

Ну так а в каком доменном объекте возникнет событие UserLogged ? По моему, это как раз тот случает когда бизнес логика не принадлежит какой либо конкретной сущности, и мы можем разместить ее в сервисе.
Соответственно их должно быть минимальное количество. Лучше если их нет вообще.
Вот тут не совсем понимаю. В реальности они будут всегда - mailer'ы, генераторы паролей и тд, в общем все для чего нужна сторонняя зависимость будет прокинуто в домен через сервис же...
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

Re: Разделение агрегата

Сообщение Bio man »

Имхо, логин, логаут и аутентификация не помещаются в рамки ни юзера, ни токена.
Как noLogicOnlyWar говорил, это скорее задача системы - выдавать токены и проверять их, работая и с юзером, и с токеном.
Так что мне больше импонирует вариант с сервисом.
Но вот тогда возникает проблема с событиями. Если в агрегатах я просто записывал события во внутренний массив и потом их получал в сервисе/репозитории, то как быть с доменным сервисом? Статические (синглтоны) публишеры мне совсем не нравятся.
Делать по аналогии? Записывать события в доменном сервисе и потом в апп сервисе их релизить?
Либо лучше сразу в доменном сервисе отдавать их диспетчеру?
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

Re: Разделение агрегата

Сообщение noLogicOnlyWar »

Ну тогда будет сервис с внутренним состоянием, вроде бы нормально.
PS пользуюсь синглтоном, так что не могу ответить.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Разделение агрегата

Сообщение sda »

noLogicOnlyWar писал(а): 2017.12.26, 13:14 Вот тут не совсем понимаю. В реальности они будут всегда - mailer'ы, генераторы паролей и тд, в общем все для чего нужна сторонняя зависимость будет прокинуто в домен через сервис же...
В Implementing domain-driven design можно найти примеры отправки email по доменному событию в application сервисе. Генерацию паролей сложно назвать бизнес-логикой. Владелец коммерческой компании врядли будет рассказывать о том, как в его бизнесе устроена генерация паролей. Это сугубо технические детали в которых бизнес-аналитик не разбирается и поскольку разработчик моделирует доменную модель реального бизнеса она должна быть лишена подобных технических деталей. Эванс пишет, что программный код хорошо спроектированной доменной модели может прочитать бизнес-аналитик поскольку код модели превращается в тот самый ubiquitous language как раз из-за отсутствия технических деталей. Прокинуть зависимость можно в application сервис, там же сгенерировать пароль и уже его передавать в доменную модель. Нет необходимости генерировать пароль внутри домена.
Имхо, логин, логаут и аутентификация не помещаются в рамки ни юзера, ни токена.
Верно ли у меня сложилось впечатление, что вы рассматриваете детали подобные сравнению хешей паролей как бизнес-логику и хотите подобные детали инкапсулировать в доменном сервисе?
Ответить