Страница 5 из 6

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

Добавлено: 2016.09.26, 13:24
zelenin
sda писал(а):То есть entity может обращаться к репозиторию ?
репозиторий и сущность - часть доменного слоя, поэтому знают друг о друге и могут обращаться.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Добавлено: 2016.09.27, 11:48
sda
ElisDN, вот я и хочу разобраться, обязательно ли нужно городить дата маппер, а поверх него репозитории. Или можно всё же упростить и сделать чтобы репозиторий напрямую делал запросы в базу и сам создавал сущности? Не хочется всё усложнять, итак проект затягивается.

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

Добавлено: 2016.09.27, 11:49
zelenin
sda писал(а):ElisDN, вот я и хочу разобраться, обязательно ли нужно городить дата маппер, а поверх него репозитории. Или можно всё же упростить и сделать чтобы репозиторий напрямую делал запросы в базу и сам создавал сущности? Не хочется всё усложнять, итак проект затягивается.
это деталь реализации. реализация может быть разной. то есть можно без маппера.

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

Добавлено: 2016.09.28, 02:57
sda
zelenin, спасибо. То есть я могу создать в репозитории метод save, который умеет сохранять как одну сущность, так и массив сущностей. Это ничего не нарушает?

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

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

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

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

Я сейчас не привязываюсь конкретно к Yii фреймворку, я просто хочу понять, правильно ли я понял, что проверка токена сброса пароля на валидность есть задача объекта (сущности) User?

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

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

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

Добавлено: 2016.09.28, 11:17
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. У вас было совершенно все по другому... :(

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

Добавлено: 2016.09.28, 12:29
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. У вас было совершенно все по другому... :(
Да, и даже в этом ответе я предложил ещё кучу вариантов. В этом и проблема работы с архитектурой, что охватить целиком всё сложно. Понимание приходит только когда все варианты сам перепробуешь :)

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

Добавлено: 2016.09.28, 13:03
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 мне понравилась. Спасибо. Но мне просто сейчас хотя бы понять, в правильном ли направлении мои мысли или нет.

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

Добавлено: 2016.09.28, 13:25
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 и прочей лапшой.

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

Добавлено: 2016.09.29, 22:12
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, т.е просить объект сохранить другой передаваемый.

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

Добавлено: 2016.10.04, 15:33
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 с заполнеными постами и ответами на посты?

Есть хоть одна ссылка во всем интернете где объясняется как воссоздавать агрегаты из базы? Не получается найти вообще ничего, как будто у меня у одного такая проблема.

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

Добавлено: 2016.10.04, 17:41
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 также отсутствуют примеры агрегатов и способы работы с ними.

В общем в теории все красиво, но кто бы на практике показал.

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

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