DDD архитектура

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

Re: DDD архитектура

Сообщение zelenin »

sda писал(а):То есть entity может обращаться к репозиторию ?
репозиторий и сущность - часть доменного слоя, поэтому знают друг о друге и могут обращаться.
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: DDD архитектура

Сообщение ElisDN »

sda писал(а):Непонятно. Дата маппер ведь уже возвращает готовую сущность, так почему нам надо воспользоваться репозиторием? Какие конкретно проблемы решает репозиторий, в отличии от дата маппера. Вот что хотелось бы услышать.
Простой маппер имеет методы, которыми умеет перегонять "объект в массив для SQL" и "SQL-результат в объект". Репозиторий же содержит кучу методов вроде find, findByEmail, findByUsername, где делает разные выборки и тоже возвращает объекты. Часто маппер - это запчасть репозитория.
Последний раз редактировалось ElisDN 2016.09.26, 18:56, всего редактировалось 1 раз.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: DDD архитектура

Сообщение zelenin »

ElisDN писал(а):
sda писал(а):Непонятно. Дата маппер ведь уже возвращает готовую сущность, так почему нам надо воспользоваться репозиторием? Какие конкретно проблемы решает репозиторий, в отличии от дата маппера. Вот что хотелось бы услышать.
Маппер имеет только два метода, которыми умеет перегонять "объект в массив для SQL" и "SQL-результат в объект". Репозиторий же содержит кучу методов вроде find, findByEmail, findByUsername, где делает разные выборки и прогоняет SQL ответы через маппер. Маппер - всего лишь запчасть репозитория.
вообще нет. это интерфейс непосредственной работы с БД, оперирующий сущностями, а не голыми скалярами. Соответственно функционал маппинга скаляров из бд к сущности там присутствует тоже, но не является частью интерфейса.
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: DDD архитектура

Сообщение ElisDN »

zelenin писал(а):вообще нет. это интерфейс непосредственной работы с БД, оперирующий сущностями, а не голыми скалярами. Соответственно функционал маппинга скаляров из бд к сущности там присутствует тоже, но не является частью интерфейса.
Если рассматривать ORM целиком, то да. Если репозиторий автора сам перегоняет из скаляров в объекты, поддерживает вставку, обновление и удаление, то он полноценным маппером данных и является. Если же репозиторий использует отдельный маппер или сторонний ORM пакет, то это работает как его запчасть. В этом случае Data Mapper обычно является более низкоуровневым, чем репозиторий в контексте домена.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: DDD архитектура

Сообщение zelenin »

ElisDN писал(а):
zelenin писал(а):вообще нет. это интерфейс непосредственной работы с БД, оперирующий сущностями, а не голыми скалярами. Соответственно функционал маппинга скаляров из бд к сущности там присутствует тоже, но не является частью интерфейса.
Если рассматривать ORM целиком, то да. Если репозиторий автора сам перегоняет из скаляров в объекты, поддерживает вставку, обновление и удаление, то он полноценным маппером данных и является. Если же репозиторий использует отдельный маппер или сторонний ORM пакет, то это работает как его запчасть. В этом случае Data Mapper обычно является более низкоуровневым, чем репозиторий в контексте домена.
это ближе к истине)
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: DDD архитектура

Сообщение ElisDN »

sda писал(а):Непонятно. Дата маппер ведь уже возвращает готовую сущность, так почему нам надо воспользоваться репозиторием? Какие конкретно проблемы решает репозиторий, в отличии от дата маппера. Вот что хотелось бы услышать.
Есть куча способов нахождения площади треугольника с математической точки зрения. Но нам для расчёта расхода шифера на фигурную крышу нужен просто какой-то "калькулятор площади".

Есть куча способов организовать фоновую очередь: Cron, Redis, RabbitMQ, at, Gearman... Но нам для понимания сути (и для объяснения этого всего какой-нибудь бабушке) нужно просто слово "очередь".

Есть несколько подходов работы с базой с технической точки зрения: Active Record, Table Gateway, Data Mapper... Но нам с точки зрения задачи не важно, что там будет внутри. Любой из них может использоваться в хранилище. Нам нужно просто понятие "хранилище".

Дело только в терминологии и уровне мышления. Одни термины сугубо программистские, а другие - человеческие. DDD как раз ориентировано на программирование человеческим языком (тот самый Ubiquitous Language). Поэтому так по-человечески вещи и называем: "хранилище", "забивальщик гвоздей", "штука, которая подписывает на рассылку" и т.п.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: DDD архитектура

Сообщение sda »

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

Re: DDD архитектура

Сообщение zelenin »

sda писал(а):ElisDN, вот я и хочу разобраться, обязательно ли нужно городить дата маппер, а поверх него репозитории. Или можно всё же упростить и сделать чтобы репозиторий напрямую делал запросы в базу и сам создавал сущности? Не хочется всё усложнять, итак проект затягивается.
это деталь реализации. реализация может быть разной. то есть можно без маппера.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: DDD архитектура

Сообщение sda »

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

Re: DDD архитектура

Сообщение ElisDN »

sda писал(а):То есть я могу создать в репозитории метод save, который умеет сохранять как одну сущность, так и массив сущностей. Это ничего не нарушает?
Нарушает типизацию и семантику. И сами методы с или ... или ... только усложняются кучами if-ов. Сделайте два отдельных метода: save(Item $item) и saveAll(array $items).
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: DDD архитектура

Сообщение sda »

ElisDN, спасибо. Скажите еще я верно понимаю что вот этот метод https://github.com/yiisoft/yii2-app-adv ... r.php#L110 есть бизнес-логика доменного объекта User ? Естественно я осознаю, что нужно убрать зависимость от Yii::$app->params['user.passwordResetTokenExpire'] и вынести её как аргумент метода, а от аргумента $token наоборот избавиться, а сам метод сделать методом объекта, вместо статичного.

Я сейчас не привязываюсь конкретно к Yii фреймворку, я просто хочу понять, правильно ли я понял, что проверка токена сброса пароля на валидность есть задача объекта (сущности) User?
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: DDD архитектура

Сообщение ElisDN »

sda писал(а):ElisDN, спасибо. Скажите еще я верно понимаю что вот этот метод isPasswordResetTokenValid есть бизнес-логика доменного объекта User ? Естественно я осознаю, что нужно убрать зависимость от Yii::$app->params['user.passwordResetTokenExpire'] и вынести её как аргумент метода, а от аргумента $token наоборот избавиться, а сам метод сделать методом объекта, вместо статичного.
Ответил в http://www.elisdn.ru/blog/94/static-method-vs-service
Последний раз редактировалось ElisDN 2016.09.28, 11:56, всего редактировалось 2 раза.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: DDD архитектура

Сообщение sda »

Я успел прочитать ваше сообщение и даже написать на него ответ. Он ниже.
--------

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

Я думал, что примерно вот так должно быть

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

namespace domain\entities;

class User
{
    private $email = null;
    private $password = null;
    private $passwordResetToken = null;
  
    public function __construct($email, $password, $passwordResetToken)
    {
        $this->email = $email;
        $this->password = $password;
        $this->passwordResetToken = $passwordResetToken;
    }
    public function assignPasswordResetToken($token)
    {
        $this->passwordResetToken = $token . '_' . time();
    }
    public function hasValidPasswordResetToken($expire = 3600)
    {
        if (empty($this->passwordResetToken)) {
            return false;
        }
        $timestamp = $this->passwordResetToken.split('_')[1];
        return timestamp + expire >= time();
    }
}
Зависимостей нет. Бизнес-логика внтури сущности. Инструкция $user->hasValidPasswordResetToken() вполне передает свой смысл. Ну или я ничего не понял в ddd. У вас было совершенно все по другому... :(
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: DDD архитектура

Сообщение ElisDN »

sda писал(а):Я успел прочитать ваше сообщение и даже написать на него ответ. Он ниже.
Там я сильно дополнил ответ. Прочитайте ещё раз.
sda писал(а):Просто вроде доменная бизнес-логика должна быть инкапсулирована в сущностях, а так получается, что всё разбросано черт знает где и получается опять анемичная модель.
Доменная логика должна быть в инкапсулирована в доменной модели. Анемичными не должны быть сущности. Вы путаете понятия "модель" и "сущность".
sda писал(а):Убедиться что токен сброса пароля является валидным, это доменная бизнес-логика.
Да, она и остаётся в домене: либо в сущности домена или в её объекте-значении (если логика простая), либо в сервисе домена (если логика сложная с кучей зависимостей и настроек).
sda писал(а):Зависимостей нет. Бизнес-логика внутри сущности.
Зависимость есть: Вам надо откуда-то передавать настройку $expire из Yii::$app->params, которую Вы закодировали как 3600 в сущности. И помимо логики Вы делаете низкоуровневый split(). Если логика работает медленно, то не сможете её мокнуть в тестах ради замены на быстрый алгоритм.
sda писал(а):Я думал, что примерно вот так должно быть
А ещё удобнее так:

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

class User
{
    public function assignPasswordResetToken($token, DateTime $expiredAt)
    {
        $this->passwordResetToken = new PasswordResetToken($token, $expiredAt);
    }
    
    public function hasValidPasswordResetToken()
    {
        if (empty($this->passwordResetToken)) {
            return false;
        }
        return $this->passwordResetToken->isValid();
    }
} 
И логика внутри сущности, и сущность не перегружена, и объект-значение PasswordResetToken подменить можно.
sda писал(а):Инструкция $user->hasValidPasswordResetToken() вполне передает свой смысл.
Если нужен именно этот метод, то можно сделать и так:

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

public function hasValidPasswordResetToken(PasswordResetTokenizerInterface $tokenizer)
{
    if (empty($this->passwordResetToken)) {
        return false;
    }
    return $tokenizer->validate($this->passwordResetToken);
} 
Получаете неанемичный метод в сущности, который делегирует проверку отдельному сервису. Тоже вариант.

А где у Вас используется метод hasValidPasswordResetToken? Какой его смысл? Слишком анемично выглядит.
sda писал(а):Ну или я ничего не понял в ddd. У вас было совершенно все по другому... :(
Да, и даже в этом ответе я предложил ещё кучу вариантов. В этом и проблема работы с архитектурой, что охватить целиком всё сложно. Понимание приходит только когда все варианты сам перепробуешь :)
Последний раз редактировалось ElisDN 2016.09.28, 14:25, всего редактировалось 1 раз.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: DDD архитектура

Сообщение sda »

Блин, split проскочил из javascript, я просто на нем пишу и немного уже забыл php и когда пишу тут примеры под php, то получается венегрет из php и javascript синтаксиса. Прошу простить за мой phpscript.

Должно было быть вместо

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

$timestamp = $this->passwordResetToken.split('_')[1];
Вот это конечно же

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

$timestamp = (int) substr($this->passwordResetToken, strrpos($this->passwordResetToken, '_') + 1);
А где у Вас используется метод hasValidPasswordResetToken? Какой его смысл?
В application layer использую, примерно вот так

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

namespace application\services;

class RestoreService {
    ...
    restore($email) {
        $user = $this->userRepository->findByEmail($email);
        if (!$user->hasValidPasswordResetToken()) {
            $token = $this->securityService->generateRandomString();
            $user->assignPasswordResetToken($token);
            $this->userRepository->save($user);
        }
        //отправка письма на емайл что-то типа $this->mailerService->send(...);
    }
}
Идея создания PasswordResetToken как Value Object мне понравилась. Спасибо. Но мне просто сейчас хотя бы понять, в правильном ли направлении мои мысли или нет.
Аватара пользователя
ElisDN
Сообщения: 5845
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: DDD архитектура

Сообщение ElisDN »

sda писал(а):В application layer использую, примерно вот так
Я бы спрятал такой анемичный геттер hasValidPasswordResetToken и анемичный сеттер assignPasswordResetToken и добавил бы доменный метод requestPasswordRestore:

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

class RestoreService {
    ...
    restore($email)
    {
        $user = $this->userRepository->findByEmail($email);
        $user->requestPasswordRestore($this->passwordTokenizer);
        $this->userRepository->save($user);
        // $this->mailerService->send(...);
    }
} 
и всю эту логику спрятал бы в сущность:

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

class User
{
    public function requestPasswordRestore(TokenizerInterface $tokenizer)
    {
        if ($this->hasValidPasswordResetToken($tokenizer)) {
            throw new \DomainException('Token is already sent.');
        }
        $this->assignPasswordResetToken($tokenizer->generate());
    }

    private function hasValidPasswordResetToken(TokenizerInterface $tokenizer)
    {
        return $tokenizer->validate($this->passwordResetToken);
    }

    ...
} 
И получил бы богатую инкапсулированую сущность без анемии и без возни с substr и прочей лапшой.
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: DDD архитектура

Сообщение slavcodev »

ElisDN писал(а):Нарушает типизацию и семантику. И сами методы с или ... или ... только усложняются кучами if-ов. Сделайте два отдельных метода: save(Item $item) и saveAll(array $items).
Могу посоветовать использовать Variadic functions в этому случае, очень smart получается, даже мощь type-hinting получаем

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

public function store(User ...$users)
{
   foreach ($users as $user) {
      $this->persist($user);
   }
}

private function persist(User $user)
{
  // Persist in storage
}

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

// Save one
$user = new User();
$repository->store($user);

// Save many
$users = [new User(), new User()];
$repository->store(...$users);
Если заботитесь о семантике, то я советую не использовать save в репозитории, если я правильно понял, save сохранения субъекта, т.е. применимо в ActiveRecord, которая сохраняется. В репозитории логичнее юзать add, store, persist, т.е просить объект сохранить другой передаваемый.
Жду Yii 3!
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: DDD архитектура

Сообщение sda »

ElisDN все равно я не пойму, как получать из базы агрегаты. Вот скажем есть Thread - корень агрегата, есть Post - сабсущность агрегата. Post без Thread не существует. Это означает, что никакой внешний код не может обращаться к Post напрямую, только косвенно через Thread, чтобы избежать противоречивого состояния агрегата. Репозиторий должен быть один на весь агрегат.

Это всё в книге эванса написано. Но там вообще никак не раскрыта идея, как загружать агрегат из базы. Например мне нужен Thread и его posts. Как я это делать должен, как-то так чтоли?

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

class ThreadRepository
{
  public function findThreadWithPosts($id, $offset, $limit)
  {
      $threadData = $this->query("SELECT * FROM threads WHERE id = :id", ['id' => $id]);
      $postsData = $this->query("SELECT * FROM posts WHERE post.thread_id = :id LIMIT :limit :offset", ['id' => $id, 'limit' => $limit, 'offset' => $offset]);
      
      return ThreadFactory::create($threadData, $postsData);
  }
}

class ThreadFactory
{
  public staic function create($threadData, $postsData = [])
  {
    $posts = [];
    
    foreach($postsData as $postData) {
      $posts[] = new Post($postData['body'], $postData['author']);
    }
    
    return new Thread($threadData['title'], $threadData['body'], $threadData['author'], $posts);
  }
}
 
Если так, то как тогда делать выборку, если цепочка зависимостей будет длинее? Например Thread->Post->Reply. Reply не существует без Post, который не существует без Thread. Какая сигнатура метода должна быть в ThreadRepository чтобы вернулся агрегат Thread с заполнеными постами и ответами на посты?

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

Re: DDD архитектура

Сообщение sda »

Я нахожу только статьи http://verraes.net/2013/12/related-enti ... -entities/ пересказывающие то что написано у эванса. И снова, автор ушел от примеров воссоздания агрегатов из базы. Он ограничился лишь следующей фразой:
For reading, we do the same: we see Project as a complete unit, including its children, so when we fetch a Project, we get the full object graph.
Из которой можно понять, что автор предлагает выгрузить Thread и 100500 его постов в память. Ну или если по его примеру, то Project и 100500 тасков. Убить сервер, вот что предлагает этот автор.

И так везде. Ноль примеров, один текст из которого мало, что понятно. С другой стороны, если загружать только часть постов/тасков, то как тогда делать такие инварианты которые предлагает автор этой статьи

Another benefit of all this encapsulation, is that Project can guard invariants for its set of Tasks. One such invariant could be a business rule stating that “Tasks can only be completed if the Project has at least five Tasks.” This business rule should not be guarded inside Task, but Project is a natural fit for it.
Эти примеры инвариантов это тоже из книги эванса. И вот если не загружаешь вместе с Project все таски, то как проверять этот инвариант соврешенно непонятно.

В ddd example https://github.com/citerus/dddsample-core также отсутствуют примеры агрегатов и способы работы с ними.

В общем в теории все красиво, но кто бы на практике показал.
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: DDD архитектура

Сообщение sda »

Или вот на php примеры https://github.com/idr0id/ddd-blog/blob ... y/User.php умеют как-то по умному выбирать вложенные сабмодели через doctrine, но я эти примеры на php вообще не понимаю. Помоему сразу понятно, что сущность здесь зависит от Doctrine, то есть от инфраструктурного уровня. Стоить его сменить и весь домен нужно переписывать. Но помоему DDD как раз о другом.
Закрыто