репозиторий и сущность - часть доменного слоя, поэтому знают друг о друге и могут обращаться.sda писал(а):То есть entity может обращаться к репозиторию ?
DDD архитектура
Re: DDD архитектура
Re: DDD архитектура
Простой маппер имеет методы, которыми умеет перегонять "объект в массив для SQL" и "SQL-результат в объект". Репозиторий же содержит кучу методов вроде find, findByEmail, findByUsername, где делает разные выборки и тоже возвращает объекты. Часто маппер - это запчасть репозитория.sda писал(а):Непонятно. Дата маппер ведь уже возвращает готовую сущность, так почему нам надо воспользоваться репозиторием? Какие конкретно проблемы решает репозиторий, в отличии от дата маппера. Вот что хотелось бы услышать.
Последний раз редактировалось ElisDN 2016.09.26, 18:56, всего редактировалось 1 раз.
Re: DDD архитектура
вообще нет. это интерфейс непосредственной работы с БД, оперирующий сущностями, а не голыми скалярами. Соответственно функционал маппинга скаляров из бд к сущности там присутствует тоже, но не является частью интерфейса.ElisDN писал(а):Маппер имеет только два метода, которыми умеет перегонять "объект в массив для SQL" и "SQL-результат в объект". Репозиторий же содержит кучу методов вроде find, findByEmail, findByUsername, где делает разные выборки и прогоняет SQL ответы через маппер. Маппер - всего лишь запчасть репозитория.sda писал(а):Непонятно. Дата маппер ведь уже возвращает готовую сущность, так почему нам надо воспользоваться репозиторием? Какие конкретно проблемы решает репозиторий, в отличии от дата маппера. Вот что хотелось бы услышать.
Re: DDD архитектура
Если рассматривать ORM целиком, то да. Если репозиторий автора сам перегоняет из скаляров в объекты, поддерживает вставку, обновление и удаление, то он полноценным маппером данных и является. Если же репозиторий использует отдельный маппер или сторонний ORM пакет, то это работает как его запчасть. В этом случае Data Mapper обычно является более низкоуровневым, чем репозиторий в контексте домена.zelenin писал(а):вообще нет. это интерфейс непосредственной работы с БД, оперирующий сущностями, а не голыми скалярами. Соответственно функционал маппинга скаляров из бд к сущности там присутствует тоже, но не является частью интерфейса.
Re: DDD архитектура
это ближе к истине)ElisDN писал(а):Если рассматривать ORM целиком, то да. Если репозиторий автора сам перегоняет из скаляров в объекты, поддерживает вставку, обновление и удаление, то он полноценным маппером данных и является. Если же репозиторий использует отдельный маппер или сторонний ORM пакет, то это работает как его запчасть. В этом случае Data Mapper обычно является более низкоуровневым, чем репозиторий в контексте домена.zelenin писал(а):вообще нет. это интерфейс непосредственной работы с БД, оперирующий сущностями, а не голыми скалярами. Соответственно функционал маппинга скаляров из бд к сущности там присутствует тоже, но не является частью интерфейса.
Re: DDD архитектура
Есть куча способов нахождения площади треугольника с математической точки зрения. Но нам для расчёта расхода шифера на фигурную крышу нужен просто какой-то "калькулятор площади".sda писал(а):Непонятно. Дата маппер ведь уже возвращает готовую сущность, так почему нам надо воспользоваться репозиторием? Какие конкретно проблемы решает репозиторий, в отличии от дата маппера. Вот что хотелось бы услышать.
Есть куча способов организовать фоновую очередь: Cron, Redis, RabbitMQ, at, Gearman... Но нам для понимания сути (и для объяснения этого всего какой-нибудь бабушке) нужно просто слово "очередь".
Есть несколько подходов работы с базой с технической точки зрения: Active Record, Table Gateway, Data Mapper... Но нам с точки зрения задачи не важно, что там будет внутри. Любой из них может использоваться в хранилище. Нам нужно просто понятие "хранилище".
Дело только в терминологии и уровне мышления. Одни термины сугубо программистские, а другие - человеческие. DDD как раз ориентировано на программирование человеческим языком (тот самый Ubiquitous Language). Поэтому так по-человечески вещи и называем: "хранилище", "забивальщик гвоздей", "штука, которая подписывает на рассылку" и т.п.
Re: DDD архитектура
ElisDN, вот я и хочу разобраться, обязательно ли нужно городить дата маппер, а поверх него репозитории. Или можно всё же упростить и сделать чтобы репозиторий напрямую делал запросы в базу и сам создавал сущности? Не хочется всё усложнять, итак проект затягивается.
Re: DDD архитектура
это деталь реализации. реализация может быть разной. то есть можно без маппера.sda писал(а):ElisDN, вот я и хочу разобраться, обязательно ли нужно городить дата маппер, а поверх него репозитории. Или можно всё же упростить и сделать чтобы репозиторий напрямую делал запросы в базу и сам создавал сущности? Не хочется всё усложнять, итак проект затягивается.
Re: DDD архитектура
zelenin, спасибо. То есть я могу создать в репозитории метод save, который умеет сохранять как одну сущность, так и массив сущностей. Это ничего не нарушает?
Re: DDD архитектура
Нарушает типизацию и семантику. И сами методы с или ... или ... только усложняются кучами if-ов. Сделайте два отдельных метода: save(Item $item) и saveAll(array $items).sda писал(а):То есть я могу создать в репозитории метод save, который умеет сохранять как одну сущность, так и массив сущностей. Это ничего не нарушает?
Re: DDD архитектура
ElisDN, спасибо. Скажите еще я верно понимаю что вот этот метод https://github.com/yiisoft/yii2-app-adv ... r.php#L110 есть бизнес-логика доменного объекта User ? Естественно я осознаю, что нужно убрать зависимость от Yii::$app->params['user.passwordResetTokenExpire'] и вынести её как аргумент метода, а от аргумента $token наоборот избавиться, а сам метод сделать методом объекта, вместо статичного.
Я сейчас не привязываюсь конкретно к Yii фреймворку, я просто хочу понять, правильно ли я понял, что проверка токена сброса пароля на валидность есть задача объекта (сущности) User?
Я сейчас не привязываюсь конкретно к Yii фреймворку, я просто хочу понять, правильно ли я понял, что проверка токена сброса пароля на валидность есть задача объекта (сущности) User?
Re: DDD архитектура
Ответил в http://www.elisdn.ru/blog/94/static-method-vs-servicesda писал(а):ElisDN, спасибо. Скажите еще я верно понимаю что вот этот метод isPasswordResetTokenValid есть бизнес-логика доменного объекта User ? Естественно я осознаю, что нужно убрать зависимость от Yii::$app->params['user.passwordResetTokenExpire'] и вынести её как аргумент метода, а от аргумента $token наоборот избавиться, а сам метод сделать методом объекта, вместо статичного.
Последний раз редактировалось ElisDN 2016.09.28, 11:56, всего редактировалось 2 раза.
Re: DDD архитектура
Я успел прочитать ваше сообщение и даже написать на него ответ. Он ниже.
--------
Просто вроде доменная бизнес-логика должна быть инкапсулирована в сущностях, а так получается, что всё разбросано черт знает где и получается опять анемичная модель. Убедиться что токен сброса пароля является валидным, это доменная бизнес-логика.
Я думал, что примерно вот так должно быть
Зависимостей нет. Бизнес-логика внтури сущности. Инструкция $user->hasValidPasswordResetToken() вполне передает свой смысл. Ну или я ничего не понял в ddd. У вас было совершенно все по другому...
--------
Просто вроде доменная бизнес-логика должна быть инкапсулирована в сущностях, а так получается, что всё разбросано черт знает где и получается опять анемичная модель. Убедиться что токен сброса пароля является валидным, это доменная бизнес-логика.
Я думал, что примерно вот так должно быть
Код: Выделить всё
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();
}
}
Re: DDD архитектура
Там я сильно дополнил ответ. Прочитайте ещё раз.sda писал(а):Я успел прочитать ваше сообщение и даже написать на него ответ. Он ниже.
Доменная логика должна быть в инкапсулирована в доменной модели. Анемичными не должны быть сущности. Вы путаете понятия "модель" и "сущность".sda писал(а):Просто вроде доменная бизнес-логика должна быть инкапсулирована в сущностях, а так получается, что всё разбросано черт знает где и получается опять анемичная модель.
Да, она и остаётся в домене: либо в сущности домена или в её объекте-значении (если логика простая), либо в сервисе домена (если логика сложная с кучей зависимостей и настроек).sda писал(а):Убедиться что токен сброса пароля является валидным, это доменная бизнес-логика.
Зависимость есть: Вам надо откуда-то передавать настройку $expire из Yii::$app->params, которую Вы закодировали как 3600 в сущности. И помимо логики Вы делаете низкоуровневый split(). Если логика работает медленно, то не сможете её мокнуть в тестах ради замены на быстрый алгоритм.sda писал(а):Зависимостей нет. Бизнес-логика внутри сущности.
А ещё удобнее так: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();
}
}
Если нужен именно этот метод, то можно сделать и так: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 раз.
Re: DDD архитектура
Блин, split проскочил из javascript, я просто на нем пишу и немного уже забыл php и когда пишу тут примеры под php, то получается венегрет из php и javascript синтаксиса. Прошу простить за мой phpscript.
Должно было быть вместо
Вот это конечно же
Идея создания PasswordResetToken как Value Object мне понравилась. Спасибо. Но мне просто сейчас хотя бы понять, в правильном ли направлении мои мысли или нет.
Должно было быть вместо
Код: Выделить всё
$timestamp = $this->passwordResetToken.split('_')[1];
Код: Выделить всё
$timestamp = (int) substr($this->passwordResetToken, strrpos($this->passwordResetToken, '_') + 1);
В application layer использую, примерно вот такА где у Вас используется метод hasValidPasswordResetToken? Какой его смысл?
Код: Выделить всё
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(...);
}
}
Re: DDD архитектура
Я бы спрятал такой анемичный геттер hasValidPasswordResetToken и анемичный сеттер assignPasswordResetToken и добавил бы доменный метод requestPasswordRestore:sda писал(а):В application layer использую, примерно вот так
Код: Выделить всё
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);
}
...
}
- slavcodev
- Сообщения: 3134
- Зарегистрирован: 2009.04.02, 21:42
- Откуда: Valencia
- Контактная информация:
Re: DDD архитектура
Могу посоветовать использовать Variadic functions в этому случае, очень smart получается, даже мощь type-hinting получаемElisDN писал(а):Нарушает типизацию и семантику. И сами методы с или ... или ... только усложняются кучами if-ов. Сделайте два отдельных метода: save(Item $item) и saveAll(array $items).
Код: Выделить всё
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);
Жду Yii 3!
Re: DDD архитектура
ElisDN все равно я не пойму, как получать из базы агрегаты. Вот скажем есть Thread - корень агрегата, есть Post - сабсущность агрегата. Post без Thread не существует. Это означает, что никакой внешний код не может обращаться к Post напрямую, только косвенно через Thread, чтобы избежать противоречивого состояния агрегата. Репозиторий должен быть один на весь агрегат.
Это всё в книге эванса написано. Но там вообще никак не раскрыта идея, как загружать агрегат из базы. Например мне нужен Thread и его posts. Как я это делать должен, как-то так чтоли?
Если так, то как тогда делать выборку, если цепочка зависимостей будет длинее? Например Thread->Post->Reply. Reply не существует без Post, который не существует без Thread. Какая сигнатура метода должна быть в ThreadRepository чтобы вернулся агрегат 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);
}
}
Есть хоть одна ссылка во всем интернете где объясняется как воссоздавать агрегаты из базы? Не получается найти вообще ничего, как будто у меня у одного такая проблема.
Re: DDD архитектура
Я нахожу только статьи http://verraes.net/2013/12/related-enti ... -entities/ пересказывающие то что написано у эванса. И снова, автор ушел от примеров воссоздания агрегатов из базы. Он ограничился лишь следующей фразой:
И так везде. Ноль примеров, один текст из которого мало, что понятно. С другой стороны, если загружать только часть постов/тасков, то как тогда делать такие инварианты которые предлагает автор этой статьи
В ddd example https://github.com/citerus/dddsample-core также отсутствуют примеры агрегатов и способы работы с ними.
В общем в теории все красиво, но кто бы на практике показал.
Из которой можно понять, что автор предлагает выгрузить Thread и 100500 его постов в память. Ну или если по его примеру, то Project и 100500 тасков. Убить сервер, вот что предлагает этот автор.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.
И так везде. Ноль примеров, один текст из которого мало, что понятно. С другой стороны, если загружать только часть постов/тасков, то как тогда делать такие инварианты которые предлагает автор этой статьи
Эти примеры инвариантов это тоже из книги эванса. И вот если не загружаешь вместе с Project все таски, то как проверять этот инвариант соврешенно непонятно.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.
В ddd example https://github.com/citerus/dddsample-core также отсутствуют примеры агрегатов и способы работы с ними.
В общем в теории все красиво, но кто бы на практике показал.
Re: DDD архитектура
Или вот на php примеры https://github.com/idr0id/ddd-blog/blob ... y/User.php умеют как-то по умному выбирать вложенные сабмодели через doctrine, но я эти примеры на php вообще не понимаю. Помоему сразу понятно, что сущность здесь зависит от Doctrine, то есть от инфраструктурного уровня. Стоить его сменить и весь домен нужно переписывать. Но помоему DDD как раз о другом.