Re: DDD архитектура
Добавлено: 2016.09.26, 13:24
репозиторий и сущность - часть доменного слоя, поэтому знают друг о друге и могут обращаться.sda писал(а):То есть entity может обращаться к репозиторию ?
репозиторий и сущность - часть доменного слоя, поэтому знают друг о друге и могут обращаться.sda писал(а):То есть entity может обращаться к репозиторию ?
Простой маппер имеет методы, которыми умеет перегонять "объект в массив для SQL" и "SQL-результат в объект". Репозиторий же содержит кучу методов вроде find, findByEmail, findByUsername, где делает разные выборки и тоже возвращает объекты. Часто маппер - это запчасть репозитория.sda писал(а):Непонятно. Дата маппер ведь уже возвращает готовую сущность, так почему нам надо воспользоваться репозиторием? Какие конкретно проблемы решает репозиторий, в отличии от дата маппера. Вот что хотелось бы услышать.
вообще нет. это интерфейс непосредственной работы с БД, оперирующий сущностями, а не голыми скалярами. Соответственно функционал маппинга скаляров из бд к сущности там присутствует тоже, но не является частью интерфейса.ElisDN писал(а):Маппер имеет только два метода, которыми умеет перегонять "объект в массив для SQL" и "SQL-результат в объект". Репозиторий же содержит кучу методов вроде find, findByEmail, findByUsername, где делает разные выборки и прогоняет SQL ответы через маппер. Маппер - всего лишь запчасть репозитория.sda писал(а):Непонятно. Дата маппер ведь уже возвращает готовую сущность, так почему нам надо воспользоваться репозиторием? Какие конкретно проблемы решает репозиторий, в отличии от дата маппера. Вот что хотелось бы услышать.
Если рассматривать ORM целиком, то да. Если репозиторий автора сам перегоняет из скаляров в объекты, поддерживает вставку, обновление и удаление, то он полноценным маппером данных и является. Если же репозиторий использует отдельный маппер или сторонний ORM пакет, то это работает как его запчасть. В этом случае Data Mapper обычно является более низкоуровневым, чем репозиторий в контексте домена.zelenin писал(а):вообще нет. это интерфейс непосредственной работы с БД, оперирующий сущностями, а не голыми скалярами. Соответственно функционал маппинга скаляров из бд к сущности там присутствует тоже, но не является частью интерфейса.
это ближе к истине)ElisDN писал(а):Если рассматривать ORM целиком, то да. Если репозиторий автора сам перегоняет из скаляров в объекты, поддерживает вставку, обновление и удаление, то он полноценным маппером данных и является. Если же репозиторий использует отдельный маппер или сторонний ORM пакет, то это работает как его запчасть. В этом случае Data Mapper обычно является более низкоуровневым, чем репозиторий в контексте домена.zelenin писал(а):вообще нет. это интерфейс непосредственной работы с БД, оперирующий сущностями, а не голыми скалярами. Соответственно функционал маппинга скаляров из бд к сущности там присутствует тоже, но не является частью интерфейса.
Есть куча способов нахождения площади треугольника с математической точки зрения. Но нам для расчёта расхода шифера на фигурную крышу нужен просто какой-то "калькулятор площади".sda писал(а):Непонятно. Дата маппер ведь уже возвращает готовую сущность, так почему нам надо воспользоваться репозиторием? Какие конкретно проблемы решает репозиторий, в отличии от дата маппера. Вот что хотелось бы услышать.
это деталь реализации. реализация может быть разной. то есть можно без маппера.sda писал(а):ElisDN, вот я и хочу разобраться, обязательно ли нужно городить дата маппер, а поверх него репозитории. Или можно всё же упростить и сделать чтобы репозиторий напрямую делал запросы в базу и сам создавал сущности? Не хочется всё усложнять, итак проект затягивается.
Нарушает типизацию и семантику. И сами методы с или ... или ... только усложняются кучами if-ов. Сделайте два отдельных метода: save(Item $item) и saveAll(array $items).sda писал(а):То есть я могу создать в репозитории метод save, который умеет сохранять как одну сущность, так и массив сущностей. Это ничего не нарушает?
Ответил в http://www.elisdn.ru/blog/94/static-method-vs-servicesda писал(а):ElisDN, спасибо. Скажите еще я верно понимаю что вот этот метод isPasswordResetTokenValid есть бизнес-логика доменного объекта User ? Естественно я осознаю, что нужно убрать зависимость от Yii::$app->params['user.passwordResetTokenExpire'] и вынести её как аргумент метода, а от аргумента $token наоборот избавиться, а сам метод сделать методом объекта, вместо статичного.
Код: Выделить всё
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();
}
}
Там я сильно дополнил ответ. Прочитайте ещё раз.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);
}
Да, и даже в этом ответе я предложил ещё кучу вариантов. В этом и проблема работы с архитектурой, что охватить целиком всё сложно. Понимание приходит только когда все варианты сам перепробуешьsda писал(а):Ну или я ничего не понял в ddd. У вас было совершенно все по другому...
Код: Выделить всё
$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(...);
}
}
Я бы спрятал такой анемичный геттер 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);
}
...
}
Могу посоветовать использовать 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);
Код: Выделить всё
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 и 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.