Давайте представим себе базовый кейс:
1. У нас есть: сущность «Дом», сущность «Квартира в доме», сущность «Застройщик», сущность «Регион».
2. У нас есть задача: иметь возможность получить всех застройщиков, иметь возможность получить все занятые регионы застройщиком, уметь возможность получить все дома, которые принадлежат застройщику и все доступные регионы вообще в принципе, где ведутся продажи домов.
3. У нас есть правила от бизнеса:
Валидный застройщик — это тот, которого мы подтвердили через админку, т.е. у которого $verifed = true.
«Но правда мы не знаем, как оно будет потом, может быть условие валидности вскоре поменяется — хз, ребята».
Валидный дом — это тот дом, у которого уже заполнились координаты и есть хотя бы какое-то описание.
«А еще чтобы была привязанна хотя бы одна квартира, у нас пока непонятно, мы думаем дома без квартир не показывать никак нигде!1! Но тут опять же, мы можем изменить понятие валидности — пока пусть будет так. Это ведь не долго потом поправить?!!! А, кстати да!!1 Если застройщик без $verifed = true, мы должны не показывать эти дома ващеее!!! Недолго же поправить?».
«А еще мы хотим чтобы показывались только те регионы, в которых есть хотя бы 1 валидный дом!!!1 И кстати такую фильтрацию надо проворачивать как на главной странице, так и на странице отдельного застройщика!!! Ты же помнишь что такое валидный дом у нас?? А??? Ты жив??»
Итак, как раньше выглядили бы мои репозитории:
class RegionRepository extends \Doctrine\ORM\EntityRepository
{
public function findAvailableRegions()
{
$qb = $this->createQueryBuilder('r');
return $qb
->join('r.houses', 'h')
->join('h.developer', 'd')
#Куча бесполезного дерьма start
->innerJoin('h.apartments', 'a') //Нам же только корректные дома нужны
->where('h.longitude IS NOT NULL') //и тут фильтруем
->andWhere('h.latitude IS NOT NULL') //блин, и тут
->andWhere('h.description IS NOT NULL') //бл*ть... это же регион.. нахера я думаю про дом здесь..
#Куча бесполезного дерьма end
#Куча бесполезного дерьма start
->andWhere('d.verified') //мне кажется я что-то делаю не так...
#Куча бесполезного дерьма end
->getQuery()
->getResult();
}
public function findAvailableRegionsByDeveloper(DeveloperCompany $developerCompany)
{
$qb = $this->createQueryBuilder('r');
return $qb
->join('r.houses', 'h')
->join('h.developer', 'd')
#Куча бесполезного дерьма start
->innerJoin('h.apartments', 'a') //Нам же только корректные дома нужны
->where('h.longitude IS NOT NULL') //и тут фильтруем
->andWhere('h.latitude IS NOT NULL') //блин, и тут
->andWhere('h.description IS NOT NULL') //бл*ть... это же регион.. нахера я думаю про дом здесь..
#Куча бесполезного дерьма end
->andWhere('d.id = :developer_id')
->setParameter('developer_id', $developerCompany->getId())
->getQuery()
->getResult();
}
}
class HouseRepository extends \Doctrine\ORM\EntityRepository
{
public function findAvailableHouses()
{
$qb = $this->createQueryBuilder('h');
return $qb
->join('h.developer', 'd')
->innerJoin('h.apartments', 'a') //фильтруем дома без квартир
->where('h.longitude IS NOT NULL') //без
->andWhere('h.latitude IS NOT NULL') //координат
->andWhere('h.description IS NOT NULL') //без описания
#опасна!!!
->where('d.verified') //черт, я ж в доме. нахера я думаю про застройщика тут...
->getQuery()
->getResult();
}
}
class DeveloperCompanyRepository extends \Doctrine\ORM\EntityRepository
{
public function findAvailableDevelopers()
{
return $this->createQueryBuilder('d')
->where('d.verified') //Дежавю........
->getQuery()
->getResult();
}
}
Итак, мы 100 раз задублировали проверку валидности застройщика по verified = true.
Сто раз задублировки проверку валидности дома по координатам, описанию и так далее.
Сто раз задублировали одновременно эти оба условия.
Изначально ты просто чешешься, временами не можешь уснуть, вспоминая об этой части твоего кода и у тебя никогда не появляется желания идти на работу с улыбкой. Но это только пока.., вот когда приходят менеджеры и меняют одно из этих условий «валидности» или ты забываешь где-то при выборке по JOIN указать тот или иной фактор валидности той или иной сущности, потому что вчера Петя вывел еще один важный момент, почему какие-то дома не должны показываться, но исправлять в 10000 репозиториях не стал и у нас теперь на странице застройщика показываются регионы, где есть невалидные дома, потому что Петя не знал о существовании подобной валидации, ибо это делал Вася — вот тогда тут уже начинает болеть в районе поясницы.
И тут, прекрасным солнечным утром ты почему-то пораньше приезжаешь на работу, когда никого нет, кофемашина готова для многократного использования — и при этом никакой очереди на нее, в офисе свежий воздух, еще не прожженый кулерами компьютеров твоих коллег и полная тишина.
В такие моменты ты становишься ближе к паттернам, и именно в такой момент ко мне пришел Specification Pattern.
Первый шаг — очистить свой разум, дабы спокойнее принять тот факт, что вам придется избавляться от $this->createQueryBuilder('alias'), воспринимать это не как какую-то революцию, а как путь в неизвестное, но в любом случае светлое будущее.
Второй шаг — composer require happyr/doctrine-specification
Третий шаг — прими факт того, что ты достоин лучшего и создай следующие класссы:
Специфика выборки валидных застройщиков.
use Happyr\DoctrineSpecification\BaseSpecification;
use Happyr\DoctrineSpecification\Spec;
class CorrectDeveloperSpecification extends BaseSpecification
{
public function getSpec()
{
return Spec::eq('verified', true);
}
}
Специфика выборки валидных домов.
use Happyr\DoctrineSpecification\BaseSpecification;
use Happyr\DoctrineSpecification\Spec;
class CorrectHouseSpecification extends BaseSpecification
{
public function getSpec()
{
Spec::andX(
Spec::innerJoin('apartments', 'a'),
Spec::innerJoin('developer', 'd'),
Spec::isNotNull('description'),
Spec::isNotNull('longitude'),
Spec::isNotNull('latitude'),
new CorrectDeveloperSpecification('d')
);
}
}
Специфика выборки валидных регионов.
use Happyr\DoctrineSpecification\BaseSpecification;
use Happyr\DoctrineSpecification\Spec;
class CorrectRegionSpecification extends BaseSpecification
{
public function getSpec()
{
return Spec::andX(
Spec::innerJoin('houses', 'h'),
new CorrectHouseSpecification('h')
);
}
}
Cпецифика выборки валидных по застройщику:
use AppBundle\Entity\DeveloperCompany;
use Happyr\DoctrineSpecification\BaseSpecification;
use Happyr\DoctrineSpecification\Spec;
class CorrectOccupiedRegionByDeveloperSpecification extends BaseSpecification
{
/** @var DeveloperCompany */
private $developer;
public function __construct(DeveloperCompany $developerCompany, $dqlAlias = null)
{
parent::__construct($dqlAlias);
$this->developer = $developerCompany;
}
public function getSpec()
{
return Spec::andX(
new CorrectRegionSpecification(),
Spec::join('developer', 'd', 'h'),
Spec::eq('d.id', $this->developer->getId())
);
}
}
Теперь самое приятное — уничтожаем, расщепляем, сжигаем говнокод из репозиториев! Прежде чем посмотреть под спойлеры, убедитесь, что вы ничем не отвлечены и готовы полностью вкусить тот факт, насколько же проще и божественней стал код…
class RegionRepository extends EntitySpecificationRepository
{
public function findAvailableRegions()
{
return $this->match(
new CorrectRegionSpecification()
);
}
public function findAvailableRegionsByDeveloper(DeveloperCompany $developerCompany)
{
return $this->match(
new CorrectOccupiedRegionByDeveloperSpecification($developerCompany)
);
}
}
class HouseRepository extends EntitySpecificationRepository
{
public function findAvailableHouses()
{
return $this->match(
new CorrectHouseSpecification()
);
}
}
class DeveloperCompanyRepository extends EntitySpecificationRepository
{
public function findAvailableDevelopers()
{
return $this->match(
new CorrectDeveloperSpecification()
);
}
}
Ну разве не конфета?
Ссылка на бундл — тут полно описания, как
Всем приятных часов кодинга, паттернов, солнечного утра и тишины в вашем Open Space.
Комментарии (81)
mrsuh
30.07.2017 09:19Для решения таких задач можно пользоваться фильтрами Doctrine
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/filters.html
https://habrahabr.ru/post/273477php_freelancer
30.07.2017 10:55Как бы вы комбинировали реализацию таких фильтров?
Для регионов нужна валидация вложенных домов, для занятых регионов — еще и по застройщикам валидация — т.е. помимо своих, надо еще и дочерние валидировать — в этом и смысл Specification Pattern.
Глобально повесить, например, фильтрацию для выборок только сущностей Region, House, DeveloperCompany, а на версии сайта застройщика еще и фильтр по застройщику давать на всё это дело — вообще не вариант, слишком неявно.
Оно хорошо, когда есть очевидные вещи — игнорировать по is_deleted=true и так далее, но бизнес я бы в такие вещи не заносил.
Fesor
30.07.2017 19:42+2фильтры доктрины вообще лучше не юзать. Никогда. Есть очень маленький процент задач где они годятся, и эти задачи никакого отношения к бизнес логике не имеют (типа вешать на них soft delete так себе идея).
michael_vostrikov
30.07.2017 10:12Я может буду тут не в тему со своим ActiveRecord, но у меня получилось вот так:
Скрытый текст$query = \common\models\Region::find() ->valid() ->whereDeveloper(1) ->with('houses') ->with('houses.apartments') ->asArray() ; var_dump($query->all()); class Region extends \yii\db\ActiveRecord { public static function tableName() { return '{{%region}}'; } public function getHouses() { return $this->hasMany(House::className(), ['region_id' => 'id']); } public static function find() { return new RegionQuery(get_called_class()); } } class RegionQuery extends \yii\db\ActiveQuery { public function valid() { return $this ->joinWith([ 'houses' => function ($query) { $query->joinWith([ 'developer' => function ($query) { $query->valid(); return $query; }, ]); $query->joinWith('apartments', true, 'INNER JOIN'); $query->valid(); return $query; }, ]); ; } public function whereDeveloper($developerId) { return $this ->joinWith([ 'houses' => function ($query) use ($developerId) { $query->where(['developer_id' => $developerId]); return $query; }, ]) ; } } class HouseQuery extends \yii\db\ActiveQuery { public function valid() { return $this ->andWhere(['is not', 'longitude', null]) ->andWhere(['is not', 'latitude', null]) ->andWhere(['is not', 'description', null]) ; } } class DeveloperQuery extends \yii\db\ActiveQuery { public function valid() { return $this->andWhere(['verified' => 1]); } }
Fesor
30.07.2017 19:44+1это не спецификация, это вы просто запихнули query в класс. Это неплохо, лучше чем размазывать все в одном месте (или еще веселее — повсюду) но все же не то.
Можете посмотреть то что надо в реализации doctrine/criteria для orm. Там через визитор это разруливается и выходит очень красиво и гибко.
michael_vostrikov
30.07.2017 20:37Спецификация или нет, но получается методы соответствуют понятиям бизнес-логики, и их можно комбинировать как нужно. Это решает исходную проблему. Решение из статьи выглядит громоздким, особенно
CorrectOccupiedRegionByDeveloperSpecification
. Не могли бы вы привести пример, как это правильно сделать через Criteria?Fesor
30.07.2017 23:21Это решает исходную проблему.
И создает новую — смешение инфраструктуры и предметной области. С другой стороны если вы используете active record у вас это и так произошло и стало быть в контексте вашего проекта это будет норм.
Решение из статьи выглядит громоздким
Это вы про нэйминг? Вообще нигде не увидел громоздкости. У меня чуть другие претензии — вроде выбор имен для элиасов, желание все проблемы решать через наследование (я чувствую тут иронию ибо профит от спецификаций именно в композиции оных) и т.д.
Не могли бы вы привести пример, как это правильно сделать через Criteria?
Criteria в случае доктрины это инфраструктура для ваших спецификаций. Оно по сути только where формирует и в чистом виде подходит только для очень простых выборок. Собственно использованная автором библиотека работает точно так же — мы создаем "запрос" как агрегацию отдельных элементов, и затем библиотечка при помощи визитора обходит граф и формирует SQL/DQL который нам нужен.
В вашем же случае например композиция запросов сильно затруднена, что влечет сложности в реюзе частей. Уж не помню как в yii работает query builder, но помниться там не так все гладко было с композицией запроса из нескольких.
michael_vostrikov
31.07.2017 08:47Это вы про нэйминг?
Нет, вопрос в том, что это получается надо на каждую комбинацию создавать отдельную спецификацию? И в целом, отдельный объект с конструктором и внутренним состоянием, который потом применяется к другому объекту с внутренним состоянием, и просто метод, добавляющий условия.
В вашем же случае например композиция запросов сильно затруднена
А приведите тогда пример такого сложного запроса на Doctrine?
VolCh
01.08.2017 11:52+1Нет, вопрос в том, что это получается надо на каждую комбинацию создавать отдельную спецификацию?
Нужно создавать спецификацию на каждое атомарное условие и(или) комбинации плохо сводимые к простым композициям or/and/not/...
Для простых композиций таких условий создавать отдельную спецификацию не обязательно, но можно, следуя обычным правилам для выделения частей приложения в отдельный метод/класс/модуль/сервис. Грубо, если вам надо только в одном месте проверить на соответствие одновременно двум атомарным спецификациям, то отдельную создавать мало смысла. Если же в десяти местах у вас используется одна и та же семиуровневая композиция из двадцати атомарных спецификаций, то лучше вынести её в отдельную.
Rathil
30.07.2017 10:51+1Лично мне первый вариант больше нравится. Конечно его можно чуть улучшить, если Вас смущают повторения. Он даёт полное представление о самом запросе, что я считаю ОЧЕНЬ важно.
zloyusr
30.07.2017 11:37+1Один из авторов Doctrine 2 еще 4 года назад описывал применение Specification Pattern в контексте Doctrine 2:
https://beberlei.de/2013/03/04/doctrine_repositories.html.
ncix
30.07.2017 19:08-2А не проще VIEW в БД написать?
agr_ugraweb
31.07.2017 11:57так и до CQRS не далеко ))
Fesor
31.07.2017 13:29+1а что в этом плохого? На самом деле в случае со системами где слово "бизнес логика" не вызывает улыбку, cqrs не сказать что сильно дороже.
boblgum
02.08.2017 11:57В первую очередь спасибо автору. Занимательно и наглядно расписал.
Но замечу, что этот паттерн ИМХО прямая дорога в ад. Ну, по крайней мере использование его в репозиториях.
По большому счету методы типа
public function findAvailableRegionsByDeveloper(DeveloperCompany $developerCompany)
как раз и есть зло. Готов поспорить что один из следующих методов будет типа
public function findAvailableRegionsByDeveloperAndSomeThingElse(DeveloperCompany $developerCompany, $somethingelse)
Причина — вы помещаете бизнес-логику в хранилище данных (см. определение репозитория). При этом единственное, что вам нужно в репозитории — фильтрация сущностей.
А вот бизнес стоило бы поместить в специфицированном сервисе.
CQRS и будет вам счастье ;) Если есть интерес, могу подробнее
oxidmod
02.08.2017 12:11+1Смысл спецификаций в репо как раз в том, чтобы не плодить методы.
В идеале будет что-то такое
public function findBySpecification(SpecificationInterface $spec) { return $this->specParser->getQuery($spec)->execute(); }
ghost404
03.08.2017 09:22Зачем
findBySpecification
если есть match?Fesor
03.08.2017 10:58потому что твои репозитории это твои репозитории, они внутри могут использовать
EntitySpecificationRepository
, но интерфейс твоих репозиториев должен быть максимально чистым.ghost404
03.08.2017 19:30Мы вроде говорим о Doctrine Specification. В Doctrine Specification ты вынужден использовать реализацию репозитория с публичными методами на подобии
match()
.
Согласен, лучше если интерфейс репозитория максимально чист и полностью принадлежит тебе, но в контексте Doctrine Specification для того чтоб этого добиться этого, придется переопределять уровень доступа для всех методов в каждом репозитории
class ArticleRepositoryDoctrine implements ArticleRepository { use EntitySpecificationRepositoryTrait { match as private; matchSingleResult as private; matchOneOrNullResult as private; getQuery as private; setAlias as private; getAlias as private; } // ... }
и еще придется реализовывать в каждом репозитории аналог
EntityRepository::createQueryBuilder()
, только приватный.Fesor
03.08.2017 22:15+1читаем про инверсию зависимостей
и еще придется реализовывать в каждом репозитории аналог EntityRepository::createQueryBuilder(), только приватный.
namespace App\Infrastructure\Doctrine; use App\Domain\{ArticleRepository, Article}; class DoctrineArticleRepository implements ArticleRepository { private $repo; private $em; public function __construct(EntityManagerInterface $em) { $this->em = $em; $this->repo = $em->getRepository(Article::class); } }
вуаля.
ghost404
04.08.2017 09:29Один репозиторий завернули в другой репозиторий.
Два репозитория на инфраструктурном уровне для одной сущности.
Да вы батенька, мастер извращений.zelenin
04.08.2017 10:54+1один. $em->getRepository(Article::class) — если вы про это, то это деталь реализации первого, в котором — о, боже — есть слово repository, хотя могло бы быть sqlConnection или thirdPartyDataMapper. И вообще не часть нашего доменного слоя.
ghost404
04.08.2017 20:09-1Давайте не будем уходить от темы статьи. Мы здесь обсуждаем Doctrine Specification.
Вернёмся к первоисточнику.
Let your repositories extend
Happyr\DoctrineSpecification\EntitySpecificationRepository
instead ofDoctrine\ORM\EntityRepository
.Это значит что вы должны создать класс
\App\Infrastructure\Doctrine\DoctrineSpecificationArticleRepository
для работы со спецификациями.
Вот об этих двух репозиториях на инфраструктурном слое я говорю.
Особенно это актуально если вы хотите в репозиторий добавить какие-то методы которых вам не хватает в основном репозитории. Для меня это countOf().
Да. Мы можем изменить базовый репозиторий для Doctrine. И даже можем создать свой базовый репозиторий наследующийся от
EntitySpecificationRepository
и добавить в него нужные нам методы. А что если нам нужно добавить методы не в общий репозиторий и не в репозиторийDoctrineArticleRepository
реализующий доменный интерфейс, а нужно создать именноDoctrineSpecificationArticleRepository
? Мы получим два репозитория на инфраструктурном слое.Fesor
04.08.2017 20:17Мы здесь обсуждаем Doctrine Specification.
это деталь реализации моих репозиториев.
Это значит что вы должны создать класс
а если тебя в ридми попросят с моста прыгнуть? вот в документации по Symfony предлагают в сущности доктрины сеттеры фичачить и забить на принцип information expert от слова совсем. Зачем они так делают? потому что иначе примеры с формами будут слишком сложные и это осознанное упрощение нацеленное на людей которым надо вув-эффект а не которые осознают что и зачем они делают.
И даже можем создать свой базовый репозиторий наследующийся от EntitySpecificationRepository и добавить в него нужные нам методы.
нас интересовать должно не это, а information hiding. Мы не добавлять должны методы а изолировать текущее. Все методы "инфраструктурные" должны быть приватными и изолированы красивым интерфейсом исключающим "неправильное использование".
Мы получим два репозитория на инфраструктурном слое.
неверно, мы получим один репозиторий в инфраструктурном слое который использует "нечто" именующееся "репозиториями" в контексте доктрины. Это разные штуки, хоть имя может тебя смущать.
ghost404
05.08.2017 00:53нас интересовать должно не это, а information hiding. Мы не добавлять должны методы а изолировать текущее. Все методы "инфраструктурные" должны быть приватными и изолированы красивым интерфейсом исключающим "неправильное использование".
Вот с изоляцией то у вас и проблемы.
Вместо того чтобы сделать один репозиторий с единой точкой доступа. Вы создали второй репозиторий и создали вторую точку доступа к данным и старательно пытаетесь оправдаться инверсией зависимостей и интерфейсом доменного слоя.
А вы не думали что доктрина может возвращать вам репозиторий реализацующий интерфейс доменного слоя?
$em->getRepository(Article::class) instanceof ArticleRepository == true;
Вы можете все так же использовать зависимости в репозитории. Все также можете внедрять репозиторий как зависимость, но ещё вы можете получать его из EntityManager-а. И точка доступа к данным у вас в этом случае одна (если не брать в расчет прямые запросы к
EntityManager
иConnection
).
use GuzzleHttp/Client; class DoctrineArticleRepository implements ArticleRepository { private $em; private $client; public function __construct(EntityManagerInterface $em, Client $client) { $this->em = $em; $this->client = $client; } public function get(ArticleId $id): Article { $article = $this->em->find(Article::class, $id); if (!$article instanceof Article) { throw new \RuntimeException(); } return $article; } public function add(Article $article): bool { $response = $this->client->request( 'put', sprintf('/article/%s/', $article->id()), ['body' => $article->text] ); return $response->getStatusCode() == 201; } }
Условный пример использования в зависимостях сервиса доменного слоя
class ArticleService { private $rep; public function __construct(ArticleRepository $rep) { $this->rep = $rep; } public function createWithText( ArticleId $id, ArticleText $text, ArticleEditor $editor ): bool { return $this->rep->add(new Article($id, $text, $editor); } }
Чувствуете разницу? Нет лишней прослойки. Вот это и есть information hiding, а не то что вы предлагаете.
И это решение, в отличии от вашего, ни сколько не нарушает использование устоявшегося и нормального способа получения репозитория из EntityManager.
Поди объясни новичку, что то, что он привык делать годами у вас делается через… иначе.
Да. Использовать EntityManager для получения репозитория не всегда хорошо, а иногда и плохо, но это лучше чем возможность получить через него репозиторий который вскрывает все кишки наружу, а не репозиторий реализующий интерфейс доменного слоя.
Сами сказали:
красивым интерфейсом исключающим "неправильное использование".
Fesor
05.08.2017 11:36+1А вы не думали что доктрина может возвращать вам репозиторий реализацующий интерфейс доменного слоя?
вот только:
$em->getRepository(Article::class) instanceof EntityRepository == true;
что меня не устраивает от слова совсем.
Вы можете все так же использовать зависимости в репозитории.
только через setter injection, что опять же меня не устраивает.
но ещё вы можете получать его из EntityManager-а.
использование entity manager-а вне репозиториев запрещено у меня на проекте. Более того — это сервис локатор, мне это не нужно.
Чувствуете разницу? Нет лишней прослойки. Вот это и есть information hiding, а не то что вы предлагаете.
не чувствую. Information hiding надо рассматривать исключительно с точки зрения клиента, в вашем случае некий ненужный
ArticleService
но этот момент опустим.
И с точки зрения клиента весь этот information hiding заканчивается на интерфейсе
ArticleRepository
. Все что внутри — это деталь реализации которую мы скрываем. Используем мы там внутри entity manager, или пользуемся репозиториями доктрины — это уже мне решать, это никак не аффектит клиентский код.
И это решение, в отличии от вашего, ни сколько не нарушает использование устоявшегося и нормального способа получения репозитория из EntityManager.
которое не рекомендуется авторами доктрины и считается плохой практикой. То что оно у вас устоялось это ваша личная проблема.
Поди объясни новичку, что то, что он привык делать годами у вас делается через… иначе.
а еще у меня сеттеров в сущностях нет, и новички въезжают в это где-то за день. Сложнее научить их и геттеры лишние не делать но это уже совсем другая проблема.
Если что — я практикую такой подход уже больше 2-х лет, и за это время на моих проектах поработало около 15-ти человек для которых "это в новинку". Проблем никогда небыло. Переобучение происходит достаточно быстро.
но это лучше чем возможность получить через него репозиторий который вскрывает все кишки наружу
это как простите? типа достать через рефлексию значение приватного вилда? Или вы намекаете что "это чудаки могут где-то заинджектить
EntityManager
в обход Dependency Injection? Ну можно настроить deptrac и бить разработчиков по рукам.
Ваш же способ просто позволяет сразу всегда и везде получать инстанс
EntityRepository
. Вообще никакой изоляции.ghost404
05.08.2017 22:51что меня не устраивает от слова совсем.
Ну это ваши личные трудности. Меня тоже много чего не устраивает и что с того?
только через setter injection, что опять же меня не устраивает.
В смысле? Использовать constructor injection религия не позволяет?
не чувствую. Information hiding надо рассматривать исключительно с точки зрения клиента
Разницы для клиента между двумя реализациями нет. Они обе дают конкретный интерфейс
ArticleRepository
.
Или вы намекаете что "это чудаки могут где-то заинджектить EntityManager в обход Dependency Injection?
А почему в обход? Если
EntityManager
вы инжектите в репозиторий, то и в любой другой сервис его можно спокойно инжектить.
Ваш же способ просто позволяет сразу всегда и везде получать инстанс EntityRepository. Вообще никакой изоляции.
Как раз наоборот. Мой вариант позволяет полностью заблокировать возможность получения
EntityRepository
. Полная изоляция.
Хотя я должен признаться. Я забыл что метод
EntityManagerInterface::getRepository()
должен возвращатьObjectRepository
. Он может возвращать и другие объекты, но это будет нарушением контракта.
Так что мой вариант нарушает контракт и ваше решение правильней, хотя я считаю его неприемлемым, от слова совсем.
oxidmod
05.08.2017 23:00А нука продемонстрируйте получение репы из ентити менеджера з доп зависимостями в контрукторе
ghost404
05.08.2017 23:37А что тут сложного?
Берём и реализовывает свойRepositoryFactory
для доктрины. Тегетируем наши сервисы репозитории и возвращаем в фабрике.
Вот простейший пример реализации фабрики репозиториев.
Fesor
06.08.2017 15:57А теперь приведи пример как мне сделать по фабрике на каждый репозиторий и сколько это потребует кода. И главный вопрос — зачем. Потому что тебе не нравится что у нас есть 2 вещи с названием "repository"? ну ок, логично что.
ghost404
06.08.2017 17:10А зачем много фабрик? Одной достаточно. Кода то всего на 10 строчек.
public function getRepository(EntityManagerInterface $entity_manager, $entity_name) { $class = $entity_manager->getClassMetadata($entity_name)->getName(); if (isset($this->ids[$class])) { return $this->container->get($this->ids[$class]); } return $this->default->getRepository($entity_manager, $entity_name); }
Fesor
07.08.2017 01:37+1А зачем много фабрик? Одной достаточно. Кода то всего на 10 строчек.
и откуда берется
ids
?ghost404
07.08.2017 09:58и откуда берется ids?
И зачем ты дурачком прикидываешся? Прекраснож понимаешь откуда.
Тегетируем наши сервисы репозитории и возвращаем в фабрике.
Естественно
ids
мы получаем из Compiler Passes.
Если тебе нужны зависимости в репозитории, то ты в любом случае должен объявить их как сервисы. А чтоб можно было их получить изEntityManager
, мы просто добавляем им метку.Fesor
07.08.2017 10:28+1services: _defaults: autowire: true public: false App\Domain\ArticleRepository: App\Infrastructure\Doctrine\DoctrineArticleRepository App\Infrastructure\Doctrine\DoctrineArticleRepository: ~
и вуаля. никаких лишних фабрик, компайл пасов, все хорошо с точки зрения разделения ответственности и соблюдения контрактов. Я все еще не понимаю почему ты считаешь такой подход "неправильным". Он требует меньше усилий со стороны разработчика, ему проще следовать, ему проще обучать. Попробуй джуну объяснить во имя чего мы переопределяли
RepositoryFactory
.ghost404
07.08.2017 20:19Всё здорово, но этого ещё нет в LTS. Со следующего года можно внедрять.
Я все еще не понимаю почему ты считаешь такой подход "неправильным".
Я уже исправился. Я уже не считаю его "неправильным". Он просто мне не нравится.
Fesor
07.08.2017 20:26Всё здорово, но этого ещё нет в LTS.
не вопрос. Хотя могли бы просто сделать надстройку над лоадером YAML например. У меня к примеру такая надстройка есть для загрузки php конфигурации.
app.infrastructure.doctrine.article_repository: class: App\Infrastructure\Doctrine\DoctrineArticleRepository autowire: true public: false autowired_types: App\Domain\ArticleRepository
Он просто мне не нравится.
чем не нравится то?
Fesor
06.08.2017 16:02Ну это ваши личные трудности. Меня тоже много чего не устраивает и что с того?
Если меня что-то не устраивает я ищу решение которое меня устраивает. И в 99% нахожу его. Есть 1% где надо идти на компромис но это не тот случай.
В смысле? Использовать constructor injection религия не позволяет?
напомню, что просто использовать constructor injection не выйдет. Для этого нужно:
- сделать свою фабрику репозиториев которая будет уметь constructor injection
- соблюсти интерфейс
ObjectRepository
- зарегистрировать отдельный сервис с использованием именно этой фабрики
- явно задать репозиторий для сущности
мне проще сделать отдельный сервис. Это решает вообще все мои проблемы без дополнительных телодвижений.
Разницы для клиента между двумя реализациями нет. Они обе дают конкретный интерфейс ArticleRepository.
именно, и мой вариант дает для клиентского кода все то же самое и с точки зрения реализации намного проще. Так зачем платить больше?
А почему в обход? Если EntityManager вы инжектите в репозиторий, то и в любой другой сервис его можно спокойно инжектить.
повторюсь — это легко выявляется на этапе код ревью или можно даже взять любой статический анализатор зависимостей. Не вижу тут проблем.
Как раз наоборот. Мой вариант позволяет полностью заблокировать возможность получения EntityRepository. Полная изоляция.
вообще-то нет, любой может получить ObjectRepository как и раньше. Ты же должен реализовать этот интерфейс у своих репозиториев.
Так что мой вариант нарушает контракт и ваше решение правильней, хотя я считаю его неприемлемым, от слова совсем.
потому что два раза слово репозиторий фигурирует или что? Что это за религия?
ghost404
05.08.2017 23:11Хотя мы можем добавить интерфейс
ObjectRepository
кDoctrineArticleRepository
и не добавлять его кArticleRepository
и таки образом не будем нарушать контракт, но смысла в этом особого нет, так как создаст ненужные, скрытые, пустые методы в репозитории.Fesor
06.08.2017 15:56Хотя мы можем добавить интерфейс ObjectRepository к
что будет полностью нарушать основную идею изоляции. Мы НЕ хотим давать доступ пользователю нашего интерфейса
ArticleRepository
кObjectRepository
. От слова совсем. Вы же предлагаете сделать именно это.
Извини, но это неверный подход.
ghost404
06.08.2017 17:03Эээ… Вы читать не умеете? Я уже тысячу раз сказал, что я не хочу давать пользователю интерфейс
ObjectRepository
. На каком ещё языке мне это сказать чтоб вы меня поняли? Может на белорусском?Fesor
07.08.2017 01:38+1вот только именно это ты и предлагаешь.
getRepository
обязан вернутьObjectRepository
. Именно это меня и удивляет.
p.s. рассмотри вариант использования deptrac на проекте для управления зависимостями и не создавай проблем на пустом месте.
VolCh
04.08.2017 12:58+1ArticleRepository не на инфраструктурном уровне, а на уровне домена. На инфраструктурном DoctrineArticleRepository, MongoArticleRepository, HttpArticleRepository и т. п.
Fesor
04.08.2017 20:15Да вы батенька, мастер извращений.
репозиторий всегда инфраструктурный слой. Реализация. А вот интерфейс оного — слой бизнес логики. Магия инверсии зависимостей. За счет этого у нас уже инфраструктура зависит от бизнес логики а не наборот.
Более того, подход который я описал:
- упрощает конфигурацию DI
- позволяет реализовать именно тот интерфейс который нужен
- прячет "доктрину" как деталь реализации инфраструктуры
- позволяет юзать не только доктрину но и скажем дергать внешние API, просто инджектишь не EM а Guzzle\Client например.
Или ты хочешь предложить мне размазывать доктрину или внешние зависимости по всему проекту?
VolCh
03.08.2017 13:57+1Вендор-специфичная вещь. Её следует помещать в public function findBySpecification(SpecificationInterface $spec)
Fesor
03.08.2017 00:24+1как раз и есть зло. Готов поспорить что один из следующих методов будет типа
так никто подобного и не предлагает. Как раз наоборот, все знания о том как происходит фильтрация выносится в спецификацию а репозиторий теперь просто просит спецификацию (а точнее адаптер под нашу СУБД) сгенерить запрос. Инфраструктура и бизнес логика разделены.
CQRS и будет вам счастье ;) Если есть интерес, могу подробнее
CQRS никак не решает эту проблему само по себе. Если вы про CQS то да, простое разделение на "чтение" и "запись" уже дают неплохой профит в плане разделения ответственности, а вот CQRS это чуть другая история со своими плюсами и минусами.
boblgum
03.08.2017 09:55тогда не стоило показывать в примере метод жестко запиленный под один UseCase.
само наименование метода фильтрации уже говорит о том, как в системе связаны сущности между собой.
findAvailableRegionsByDeveloper
отношения такого рода диктуются бизнесом. если у вас завтра бизнес отменит связку Developer->Region, что вы будете делать?
И вот как раз CQRS помогает держать логику бизнес-процесса и его отработку в точно определенном месте (Command/Query-Handler). Если меняется процесс, то меняется только его обработка, но никак не хранилище данных.
Опят таки, как вы решите проблему фильтрации сугубо на уровне репозитория, если ваши данные хранятся в разном виде одновременно? Регионы в базе, а девелоперы, например, в виде почтовых аккаунтов.Fesor
03.08.2017 11:17+2Если меняется процесс, то меняется только его обработка, но никак не хранилище данных.
это особенность event sourcing нежели CQRS. В этом ключе у нас есть ивенты по которым мы всегда можем построить проекцию данных под задачу. CQRS можно строить и без event sourcing, а вот последнее без первого уже проблематично.
Опят таки, как вы решите проблему фильтрации сугубо на уровне репозитория, если ваши данные хранятся в разном виде одновременно?
давайте рассуждать. Зачем нам нужны репозитории и что они должны возвращать? Эта штука которая скрывает persistence layer от слоя бизнес логики и позволяет развернуть зависимость что бы наша бизнес логика вообще не зависела от инфраструктуры.
Соответственно что могут возвращать репозитории — они должны возвращать сущность, или же VO с какой-то статистикой по всей коллекции сущностей. То есть репозиторий не должен нам возвращать коллекции, только что-то в единичном экземпляре.
Когда нам нужно получить коллекцию сущностей вне другой сущности? Обычно это связано с отображением всякого рода списочков, поисковых выборок и т.д. На этот вид задач очень похожи репорты. Запихивать логику формирования репорта который включает различные сущности в какой-то репозиторий — как минимум некорректно. Точно так же пихать выборку по разным сущностям (что бы список составить) в репозиторий какой-то одной из них не очень корректно. По хорошему репозитории мы должны делать на корень агрегата, а не на списочек.
А потому мы можем сделать что-то типа read-model, которой не нужны репозитории поскольку оно read-only. И для проблемы озвученной вами мы можем делать свои проекции этих данных для более удобной работы. Это все еще и ни CQRS и ни event sourcing. Это больше походит на принцип CQS Мэйерса.
boblgum
03.08.2017 12:44Event Sourcing как раз и есть вариант сохранения данных и/или состояния системы. А вот CQRS отвечает за реализацию бизнес-процессов.
CQRS можно строить и без event sourcing, а вот последнее без первого уже проблематично.
Почему? Два концепта описывающие совершенно разные части системы.
You can use event sourcing without also applying the CQRS pattern. The ability to rebuild the application state, to mine the event history for new business data, and to simplify the data storage part of the application are all valuable in some scenarios.
https://msdn.microsoft.com/en-us/library/jj591559
Репозиторий
Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.
https://martinfowler.com/eaaCatalog/repository.html
То есть репозиторий и есть тот самый списочек
То есть репозиторий не должен нам возвращать коллекции, только что-то в единичном экземпляре.
Это с какого перепугу?
Fesor
03.08.2017 18:32-1Почему? Два концепта описывающие совершенно разные части системы.
Я Грэгу Янгу больше в этих вопросах доверяю. Как никак первоисточник. ES без CQRS невозможно. И CQRS можно делать без command/query bus и прочих вещей. Опять же отсылка к первоисточникам — Бертранд Мэйер и его CQS.
Это с какого перепугу?
Если мы говорим про репозитории в контексте бизнес логики — потому что они нужны нам что бы корень агрегата собрать. А дальше сама сущность со всем справится.
boblgum
03.08.2017 19:28Я Грэгу Янгу больше в этих вопросах доверяю
ES без CQRS невозможно.Уже не первый раз слышу. Можно пожалуйста ссылку, где первоисточник подтверждает ваш тезис?
Кстати, предисловие к книге на MSDN написал как раз Грэг Янг.
https://msdn.microsoft.com/en-us/library/jj591564.aspx
что бы корень агрегата собрать.
Вообще-то определение репозитория как паттерна ничего о корнях и агрегатах не говорит. То о чем вы говорите не соответствует определению репозитория. Ну или вы его зачем то дико урезаете, пытаясь его использовать. Тут конечно хозяин — барин.
VolCh
04.08.2017 13:01Вообще-то определение репозитория как паттерна ничего о корнях и агрегатах не говорит.
Если в домене есть агрегаты с корнем и "листьями", то репозиторий должен быть только для корня по определению агрегата. Если мы делаем репозиторий для "листьев", то нарушаем принцип агрегата.
boblgum
03.08.2017 20:32Ещё насчёт первоисточника
Nothing is new, Event Sourcing has been around since the 70s.
http://codebetter.com/gregyoung/2010/02/16/cqrs-task-based-uis-event-sourcing-agh/
Не поленитесь, прошу, и сами проверьте, кто же это сказал.Fesor
03.08.2017 22:17да, но вы именно CQRS предлагает как решение всех проблем, а не конкретно event sourcing.
boblgum
02.08.2017 12:18Абсолютно согласен.
При этом единственное, что вам нужно в репозитории — фильтрация сущностей.
то есть как раз ваш идеал и реализует то, что я сказал. а вот методы типа
findAvailableRegionsByDeveloper
это уже в сторону бизнес-логики
Fesor
03.08.2017 00:25+1это уже в сторону бизнес-логики
если это что-то возвращает нам коллекцию — то нет. 90% что это нужно только для репрезентации данных (read model если хотите), и еще 10% что мы неверно выбрали корень агрегата где-то.
boblgum
03.08.2017 10:06а если ничего не найдем и решили возвращать
null
вместо пустой коллекции? при чем тут коллекция? само присутствие метода с таким именем в репозитории уже проблема или по крайней мере симптом.
Ваш read model должен тогда быть примерно таким
AvailableRegionsByDeveloper { public function __constructor(Developer $dev) { } public function all() { ... } }
Fesor
03.08.2017 11:21а если ничего не найдем и решили возвращать null вместо пустой коллекции?
тогда я бы отправил человека почитать о профите null-object-ов
при чем тут коллекция?
я мне кажется достаточно точно описал причем тут коллекции и как они показывают симптомы наших репозиториев.
Ваш read model должен тогда быть примерно таким
да, что-то типа такого. Но если подумать, то почти все эти модели будут содержать очень похожие по смыслу вещи, а от того проще будет сделать точку входа и кидать туда объекты которые уже занимаются описанием выборки. И это именно то чем являются спецификации.
А проблему с разграничением зон ответственностей, проекциями и т.д. мы решаем уже на чуть другом уровне.
boblgum
03.08.2017 12:57+1Почитайте еще раз определение самого паттерна
Conceptually, a Repository encapsulates the set of objects
https://martinfowler.com/eaaCatalog/repository.html
Там где есть полный список, есть и частичный. Одно другому ну никак не мешает.
А с остальным согласен и ничего не хотел сказать против. Согласно паттерну репозитория, выборки декларируются вне репозитория
Client objects construct query specifications declaratively and submit them to Repository
Fesor
03.08.2017 22:21Там где есть полный список, есть и частичный.
В этом случае у вас по сути "репозиторий" на каждую выборку (что мы выше и обсуждали) что в целом меня устраивает и я даже так иногда делаю.
oxidmod
Выглядит здорово, но давайте посмотрим еще раз.
Чем по сути является спецификация? По сути это бизнес-правила. А раз так, то и определяться они должны в слое бизнес-логики. Но вот бизнес логике нет дела до персистент-слоя. Он не должен знать что там доктрина под капотом. Может там вообще файлики или ин-мемори хранилище. А эти спецификации тащат в бизнес-логику знания о СУБД и доктрине
php_freelancer
У меня очень мало опыта с DDD, пока имеется только лишь громадный интерес ко всей этой теме и боль в спине от несостыковок между конечным вариантом фичи и волшебными требованиями в голове заказчика и арт директора .)
Ну а так все верно, тут Domain Layer и Persistence Layer в достаточной мере размыты. Но опять же, применять Specification Pattern уже на коллекцию сгидрированных со стоража доменов не получится в силу банального fatal out of memory error.))
Не у всех есть DDD и такой вариант со спецификациями я думаю лучше чем первый, когда дублируются эти правила в репозиториях.
Вообще, если с точки зрения DDD это не какой-то банальный кейс, я бы очень хотел услышать ваше мнение, как и что Вы бы применили для решения подобных проблем.
VolCh
Вы можете в слое бизнес-логики определить «наивную» спецификацию как POPO, проверяющий удовлетворяет ли конкретный инстанс сущности, используя её и в репозитории для фильтрации результатов запроса, а затем расширить в слое инфраструктуры для модификации запроса до его отправки.
Да, придётся поддерживать две формы записи одного условия и не забывать менять их синхронно, но это уже будет шагом вперёд по сравнению с хардкодингом запроса в репозитории.
bighoc
Посмотрел еще раз.
» и использовать его же для построения запросов создает двунаправленную зависимость между модулями (слоями).Проблема дублирования условия «verified = true» и подобных решена — все инкапсулировано внутри репозитория. Если захотим, изменить правила для застройщика — запросто, вся логика для этого в CorrectDeveloperSpecification.
Создаем *RepositoryInterface и завязываемся на него. Если захотим, можем подкидывать реализацию этого же репозитория для «ин-мемори хранилища» или любого другого (будем честными — «файлики»???!!!).
А вот создавать классы
VolCh
Основная ответственность спецификации — именно проверка инстанса объекта на соответствие заложенной в неё бизнес-логике. Использование спецификации для построения запросов в виде передачи запроса/его билдера для модификации в лучшем случае — получение представления спецификации в виде понятном механизму запросов. Если не дополнительная полноценная ответственность, то оптимизация для конкретного кейса выборки с помощью запроса.
По хорошему, спецификация вообще не должна зависеть от используемого механизма хранения, даже знать о его наличии, а этот механизм должен по спецификации обеспечивать выборки как-то сам, зная как получить условия в терминах бизнес-логики и как отмаппить её на механизм хранения. Знания спецификации о механизме хранения в принципе протекающая абстракция, а уж знание о деталях её реализации — прямо таки водопад.
ghost404
Натыкался на реализацию спецификаций на .NET через Expression.
Я не знаком с .NET, но на сколько я могу судить, такие спецификации могут как проверять конкретный инстанс, так и могут быть трансформированы в SQL.
Интересно былоб увидеть что-то подобное в PHP.
VolCh
Есть DSL такой 'group in :groups and points > :min_points'; https://github.com/K-Phoen/rulerz/blob/master/doc/writing_rules.md
Работает с нативными объектами и массивами, с доктриной, с эластиком и т. д.
ghost404
Это RulerZ. Я знаю о нем и о нем уже упоминали ниже. Мне он как-то не очень нравится.
Надо будет при случае по глубже изучить.
VolCh
Мне не нравится в нём сам синтаксис DSL, но принцип вполне, если не единственный разумный: