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

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

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

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

Есть у меня агрегат StaffMember (сотрудник), который хранит в себе данные, которые слабо связаны (low cohesion): анкетные данные (имя, email итд) и данные аутентификации (пароль, токены аутентификации, токен сброса пароля итп).
Сделал 3 сервиса, 1 для переименований, смен статуса итд, 2 для контроля доступа, 3 для сброса пароля.
Но агрегат не хило так разбух.
Может можно как-то разделить на разные контексты?
Например, в контексте фронтенда мне нужны только анкетные данные, все остальное нужно только для админки.

Как лучше поступить?

Для наглядности:

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

<?php

namespace App\Domain\Staff\Entities\StaffMember;

class StaffMember implements AggregationRootInterface
{
    use EventsTrait;

    private $id;
    private $email;
    private $passwordHash;
    private $name;
    private $role;
    private $statuses;
    /** @var \ArrayObject|AuthenticationToken[]  */
    private $authenticationTokens;

    private $creationTime;

    public function __construct(
        Id $id,
        Email $email,
        PasswordHash $passwordHash,
        Name $name,
        Role $role
    )
    { ... }

    public function getId(): Id { ... }

    public function getEmail(): Email { ... }

    public function getPasswordHash(): PasswordHash { ... }

    public function getName(): Name { ... }

    public function getRole(): Role { ... }

    public function getCreationTime(): \DateTimeImmutable { ... }

    /**
     * @return Status[]
     */
    public function getStatuses(): array { ... }

    /**
     * @return AuthenticationToken[]
     */
    public function getAuthenticationTokens(): array { ... }

    public function isActive(): bool { ... }

    public function isArchived(): bool { ... }

    public function rename(Name $name): void { ... }

    public function changeEmail(Email $email): void { ... }

    public function changeRole(Role $role): void { ... }

    public function archive(\DateTimeImmutable $time): void { ... }

    public function reinstate(\DateTimeImmutable $time): void { ... }

    public function revokeAccess(string $token): void { ... }

    public function authenticate(string $token): void { ... }

    public function logout(string $token): void { ... }

    public function login(AuthenticationToken $token): void { ... }
}
Nex-Otaku
Сообщения: 831
Зарегистрирован: 2016.07.09, 21:07

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

Сообщение Nex-Otaku »

Разделяй.
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

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

Сообщение noLogicOnlyWar »

login, logout, authenticate точно можно вынести в доменный сервис, Authentifier какой нибудь.
Так же archive/reinstate, если я правильно понял, тоже можно в доменный сервис. Ну и revokeAccess, хотя тут по разному можно.
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

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

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

noLogicOnlyWar писал(а): 2017.12.25, 11:16 login, logout, authenticate точно можно вынести в доменный сервис, Authentifier какой нибудь.
Так же archive/reinstate, если я правильно понял, тоже можно в доменный сервис. Ну и revokeAccess, хотя тут по разному можно.
Причем тут сервисы?
Я спрашиваю, как бы мне разделить агрегат правильно, а не как организовать сервисы.
И ничего тут выносить не надо, все эти методы являются поведением агрегата, т.е. изменяют его состояние.
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

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

Сообщение noLogicOnlyWar »

login, logout изменяют внутреннее состояние пользователя?
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

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

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

Да, логин добавляет токен аутентификации, логаут его удаляет. authenticate обновляет токен, выставляя ему новое время последнего доступа (last activity time).
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

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

Сообщение noLogicOnlyWar »

Ну так почему не перенести ответственность в другой класс? Как еще делать декомпозицию год объекта? Токенами должен обязательно юзер управлять?
И еще по сути вопроса, админка/фронтенд это ведь чисто инфраструктурные вещи, в домене у вас контекст в котором работает модель = bounded context.
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

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

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

Почему бы и нет? Хотя, я не утверждаю. Токены нужны только для админки, на фронте от юзера нужны только имя и почта.
Как разделить? Будет ли отдельный агрегат для админки, который управляет токенами юзера, и отдельный общий агрегат, который управляет статусами, переименованием итд?
Или токен сделать отдельным агрегатом содержащим в себе юзера?
Хз, как правильно.
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

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

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

noLogicOnlyWar писал(а): 2017.12.25, 16:53 И еще по сути вопроса, админка/фронтенд это ведь чисто инфраструктурные вещи, в домене у вас контекст в котором работает модель = bounded context.
Вот с контекстами я практически незнаком. Есть пример выделения контекстов с примерами организации этого всего в коде?
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

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

Сообщение noLogicOnlyWar »

Bio man писал(а): 2017.12.25, 17:05 Вот с контекстами я практически незнаком. Есть пример выделения контекстов с примерами организации этого всего в коде?
примеры с bounded context есть в конце ddd in php, правда к вашему вопросу это не особо применимо, помойму. Я это к тому что домен не должен знать в админке ли его модель используется, или во фронтенде - это 100%.
Как разделить? Будет ли отдельный агрегат для админки, который управляет токенами юзера, и отдельный общий агрегат, который управляет статусами, переименованием итд?
Ну вот я предлагаю ввести AuthentifierManager например, апи на ружу будет login, logout, authenticate; реализация будет сокрыта внутри -
генерировать сохранять и обновлять токен. Соотв. у него должна быть зависимость на репозиторий с токенами и он будет знать о аггрегате юзер. Вроде бы все.
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

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

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

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

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

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

Сообщение noLogicOnlyWar »

Нет, думаю пароль должен быть в юзере, но генерация пароля в доменном сервисе который будет реализовываться в инфраструктурном слое, скорее всего.
Кстати по поводу bounded context, тема сложна, возможно frontend и admin можно рассматривать как 2 разных контекста, но тогда соответственно у нас будут и 2 сущности юзера, никак не связанные между собой тк будут находится в 2х разных доменах. Старшие коллеги помогут разобраться тут надеюсь :) . И возникает вопрос нужно ли тебе так систему разделять, если предположение выше верно.
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

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

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

noLogicOnlyWar писал(а): 2017.12.25, 18:00 И возникает вопрос нужно ли тебе так систему разделять, если предположение выше верно.
На бекенд и фронтенд? Нужно. На фронте совсем другая сущность будет рулить.

ElisDN, zelenin, призываю вас! Явитесь нам! :)
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

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

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

Если я отделю токены от юзера, то львинная доля бизнес логики переедит в сервис.

То, что есть сейчас, примеры кода:

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

    // StaffMember
    public function login(AuthenticationToken $token): void
    {
        if ($this->isArchived()) {
            throw new AuthenticationFailedException('Archived staff members can not be logged in.');
        }

        if (isset($this->authenticationTokens[$token->getToken()])) {
            throw new AlreadyLoggedInException();
        }

        $this->assignAuthenticationToken($token);
        $this->recordEvent(new Events\LoggedIn($this->id, $token));
    }
    
    
    // Auth service
    public function login(LoginDto $dto, \DateTimeImmutable $expirationTime): string
    {
        $staffMember = $this->repository->getActiveByEmail($dto->email);

        if (!$this->passwordSecurity->validate($dto->password, $staffMember->getPasswordHash())) {
            throw new InvalidPasswordException();
        }

        $token = $this->generateToken();
        $staffMember->login(new AuthenticationToken($token, new \DateTimeImmutable(), $expirationTime));

        $this->repository->save($staffMember);
        $this->eventDispatcher->dispatch(...$staffMember->releaseEvents());

        return $token;
    }
Тогда логика из сущности перекочует в сервис.
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

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

Сообщение noLogicOnlyWar »

Я бы еще вынес в доменный сервис архивацию/восстановление. У себя так и делал, но у меня сущность и сущность в архиве это 2 разные сущности (через sti) тк сущность в архиве меняет свое поведение и тоже участвует в бизнес логике.
Тогда логика из сущности перекочует в сервис.
Но она все равно будет рядом с сущностью лежать. К тому же логика из application сервиса (// Auth service который) тоже перейдет в домен.
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

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

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

Не, что-то не складывается... Какое поведение у токена может быть? Да ни какого, это просто VO.
Ни он залогинить может, ни аутентифицировать.
Другое дело юзер, он может, и, имхо, должен управлять своими токенами, т.к. может залогиниться, выйти из системы, удалить определенный токен (если хочет, что бы на конкретном устройстве он не был аутентифицирован) итп.
Но все эти плюшки нужны на бекенде. На фронте, по сути и поведения никакого не будет, только отображение данных юзера.

Насчет архивации и восстановления не соглашусь. Эти методы меняют состояние сущности, значит должны быть в ней. Если делать как ты говоришь, то в сервисе придется дергать $user->setState(State::ARCHIVED), что, имхо, отклонение от ubiquitous language
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

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

Сообщение noLogicOnlyWar »

Не, что-то не складывается... Какое поведение у токена может быть? Да ни какого, это просто VO.
Да никакого, а в чем не состыковка? Ну юзер тоже сам себя не залогинит в систему, помойму это система (в данном случае authentificator) должна принять решение залогинить юзера или отклонить.

Вот чтобы не быть голословным, Authentifier тут имеется
https://github.com/dddinphp/last-wishes
https://github.com/dddinphp/last-wishes ... main/Model
Насчет архивации и восстановления не соглашусь. Эти методы меняют состояние сущности, значит должны быть в ней. Если делать как ты говоришь, то в сервисе придется дергать $user->setState(State::ARCHIVED), что, имхо, отклонение от ubiquitous language
У меня не так, просто $enitityArchive->add($enitity). По сравнению с твоим вариантом субьект и объект инвертирован.
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

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

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

Значит ты предлагаешь задачи аутентификации вынести в app service, который будет получать юзера, делать все проверки, запрашивать у доменного сервиса токен и назначать его юзеру?
noLogicOnlyWar
Сообщения: 83
Зарегистрирован: 2017.07.04, 20:53

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

Сообщение noLogicOnlyWar »

Нет, это доменный сервис
Bio man
Сообщения: 609
Зарегистрирован: 2013.07.22, 10:40

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

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

Доменные сервисы могут вызываться напрямую из presentation layer?
Т.е. могут принимать DTO?
Ответить