Pull to refresh

Comments 90

UFO just landed and posted this here
У меня очень мало опыта с DDD, пока имеется только лишь громадный интерес ко всей этой теме и боль в спине от несостыковок между конечным вариантом фичи и волшебными требованиями в голове заказчика и арт директора .)

Ну а так все верно, тут Domain Layer и Persistence Layer в достаточной мере размыты. Но опять же, применять Specification Pattern уже на коллекцию сгидрированных со стоража доменов не получится в силу банального fatal out of memory error.))

Не у всех есть DDD и такой вариант со спецификациями я думаю лучше чем первый, когда дублируются эти правила в репозиториях.

Вообще, если с точки зрения DDD это не какой-то банальный кейс, я бы очень хотел услышать ваше мнение, как и что Вы бы применили для решения подобных проблем.
Вы можете в слое бизнес-логики определить «наивную» спецификацию как POPO, проверяющий удовлетворяет ли конкретный инстанс сущности, используя её и в репозитории для фильтрации результатов запроса, а затем расширить в слое инфраструктуры для модификации запроса до его отправки.

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

Проблема дублирования условия «verified = true» и подобных решена — все инкапсулировано внутри репозитория. Если захотим, изменить правила для застройщика — запросто, вся логика для этого в CorrectDeveloperSpecification.
Создаем *RepositoryInterface и завязываемся на него. Если захотим, можем подкидывать реализацию этого же репозитория для «ин-мемори хранилища» или любого другого (будем честными — «файлики»???!!!).

А вот создавать классы
«проверяющие удовлетворяет ли конкретный инстанс сущности
» и использовать его же для построения запросов создает двунаправленную зависимость между модулями (слоями).

Основная ответственность спецификации — именно проверка инстанса объекта на соответствие заложенной в неё бизнес-логике. Использование спецификации для построения запросов в виде передачи запроса/его билдера для модификации в лучшем случае — получение представления спецификации в виде понятном механизму запросов. Если не дополнительная полноценная ответственность, то оптимизация для конкретного кейса выборки с помощью запроса.


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

Натыкался на реализацию спецификаций на .NET через Expression.
Я не знаком с .NET, но на сколько я могу судить, такие спецификации могут как проверять конкретный инстанс, так и могут быть трансформированы в SQL.
Интересно былоб увидеть что-то подобное в PHP.

Это RulerZ. Я знаю о нем и о нем уже упоминали ниже. Мне он как-то не очень нравится.
Надо будет при случае по глубже изучить.

Мне не нравится в нём сам синтаксис DSL, но принцип вполне, если не единственный разумный:


  • задаём условия каким-то универсальным способом, внутри сведенным к AST
  • пишем адаптеры по применению условий к каким-то конкретным поставщикам данным
  • чтобы каждый раз не парсить условия делаем компилятор
Для решения таких задач можно пользоваться фильтрами Doctrine
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/filters.html
https://habrahabr.ru/post/273477
Как бы вы комбинировали реализацию таких фильтров?

Для регионов нужна валидация вложенных домов, для занятых регионов — еще и по застройщикам валидация — т.е. помимо своих, надо еще и дочерние валидировать — в этом и смысл Specification Pattern.

Глобально повесить, например, фильтрацию для выборок только сущностей Region, House, DeveloperCompany, а на версии сайта застройщика еще и фильтр по застройщику давать на всё это дело — вообще не вариант, слишком неявно.

Оно хорошо, когда есть очевидные вещи — игнорировать по is_deleted=true и так далее, но бизнес я бы в такие вещи не заносил.

фильтры доктрины вообще лучше не юзать. Никогда. Есть очень маленький процент задач где они годятся, и эти задачи никакого отношения к бизнес логике не имеют (типа вешать на них soft delete так себе идея).

Я может буду тут не в тему со своим 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]);
    }
}


Получается такой запрос:
Скрытый текст
SELECT `region`.*
FROM `region`
  LEFT JOIN `house` ON `region`.`id` = `house`.`region_id`
  LEFT JOIN `developer` ON `house`.`developer_id` = `developer`.`id`
  INNER JOIN `apartment` ON `house`.`id` = `apartment`.`house_id`
WHERE
  (`longitude` IS NOT NULL) AND (`latitude` IS NOT NULL) AND (`description` IS NOT NULL)
  AND (`verified`=1)
  AND (`developer_id`=1)


Выглядит довольно лаконично. С Доктриной особо не знаком, думал там есть похожие способы это сделать.

это не спецификация, это вы просто запихнули query в класс. Это неплохо, лучше чем размазывать все в одном месте (или еще веселее — повсюду) но все же не то.


Можете посмотреть то что надо в реализации doctrine/criteria для orm. Там через визитор это разруливается и выходит очень красиво и гибко.

Спецификация или нет, но получается методы соответствуют понятиям бизнес-логики, и их можно комбинировать как нужно. Это решает исходную проблему. Решение из статьи выглядит громоздким, особенно CorrectOccupiedRegionByDeveloperSpecification. Не могли бы вы привести пример, как это правильно сделать через Criteria?

Это решает исходную проблему.

И создает новую — смешение инфраструктуры и предметной области. С другой стороны если вы используете active record у вас это и так произошло и стало быть в контексте вашего проекта это будет норм.


Решение из статьи выглядит громоздким

Это вы про нэйминг? Вообще нигде не увидел громоздкости. У меня чуть другие претензии — вроде выбор имен для элиасов, желание все проблемы решать через наследование (я чувствую тут иронию ибо профит от спецификаций именно в композиции оных) и т.д.


Не могли бы вы привести пример, как это правильно сделать через Criteria?

Criteria в случае доктрины это инфраструктура для ваших спецификаций. Оно по сути только where формирует и в чистом виде подходит только для очень простых выборок. Собственно использованная автором библиотека работает точно так же — мы создаем "запрос" как агрегацию отдельных элементов, и затем библиотечка при помощи визитора обходит граф и формирует SQL/DQL который нам нужен.


В вашем же случае например композиция запросов сильно затруднена, что влечет сложности в реюзе частей. Уж не помню как в yii работает query builder, но помниться там не так все гладко было с композицией запроса из нескольких.

Это вы про нэйминг?

Нет, вопрос в том, что это получается надо на каждую комбинацию создавать отдельную спецификацию? И в целом, отдельный объект с конструктором и внутренним состоянием, который потом применяется к другому объекту с внутренним состоянием, и просто метод, добавляющий условия.


В вашем же случае например композиция запросов сильно затруднена

А приведите тогда пример такого сложного запроса на Doctrine?

Нет, вопрос в том, что это получается надо на каждую комбинацию создавать отдельную спецификацию?

Нужно создавать спецификацию на каждое атомарное условие и(или) комбинации плохо сводимые к простым композициям or/and/not/...


Для простых композиций таких условий создавать отдельную спецификацию не обязательно, но можно, следуя обычным правилам для выделения частей приложения в отдельный метод/класс/модуль/сервис. Грубо, если вам надо только в одном месте проверить на соответствие одновременно двум атомарным спецификациям, то отдельную создавать мало смысла. Если же в десяти местах у вас используется одна и та же семиуровневая композиция из двадцати атомарных спецификаций, то лучше вынести её в отдельную.

Лично мне первый вариант больше нравится. Конечно его можно чуть улучшить, если Вас смущают повторения. Он даёт полное представление о самом запросе, что я считаю ОЧЕНЬ важно.
View и Doctrine не очень хорошо работают вместе

А что в них плохо работает вместе?

видимо человек писать пытался в них

а что в этом плохого? На самом деле в случае со системами где слово "бизнес логика" не вызывает улыбку, cqrs не сказать что сильно дороже.

согласен, тут вопрос лишь в масштабе ИС

Если кому интересно, есть подвижки по отделению Doctrine Specification от Doctrine (1, 2).
На сколько это осмыслено большой вопрос, но работа в этом направлении велась.

проще новую либу будет сделать… ибо с 14-ого года много воды утекло.

UFO just landed and posted this here

да, тоже тыкал и в целом норм.

Об это я в общем-то и написал ментейнеру.
Если отделять Doctrine от Doctrine Specification, то это должен быть как минимум отдельный репозиторий и по сути новая либа с сохранением некоторых старых методов пользования.

В первую очередь спасибо автору. Занимательно и наглядно расписал.
Но замечу, что этот паттерн ИМХО прямая дорога в ад. Ну, по крайней мере использование его в репозиториях.
По большому счету методы типа


public function findAvailableRegionsByDeveloper(DeveloperCompany $developerCompany)

как раз и есть зло. Готов поспорить что один из следующих методов будет типа


public function findAvailableRegionsByDeveloperAndSomeThingElse(DeveloperCompany $developerCompany, $somethingelse)

Причина — вы помещаете бизнес-логику в хранилище данных (см. определение репозитория). При этом единственное, что вам нужно в репозитории — фильтрация сущностей.
А вот бизнес стоило бы поместить в специфицированном сервисе.


CQRS и будет вам счастье ;) Если есть интерес, могу подробнее

UFO just landed and posted this here

потому что твои репозитории это твои репозитории, они внутри могут использовать EntitySpecificationRepository, но интерфейс твоих репозиториев должен быть максимально чистым.

Мы вроде говорим о 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(), только приватный.

читаем про инверсию зависимостей


и еще придется реализовывать в каждом репозитории аналог 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);
     }
}

вуаля.

Один репозиторий завернули в другой репозиторий.
Два репозитория на инфраструктурном уровне для одной сущности.
Да вы батенька, мастер извращений.

один. $em->getRepository(Article::class) — если вы про это, то это деталь реализации первого, в котором — о, боже — есть слово repository, хотя могло бы быть sqlConnection или thirdPartyDataMapper. И вообще не часть нашего доменного слоя.

Давайте не будем уходить от темы статьи. Мы здесь обсуждаем Doctrine Specification.
Вернёмся к первоисточнику.


Let your repositories extend Happyr\DoctrineSpecification\EntitySpecificationRepository instead of Doctrine\ORM\EntityRepository.

Это значит что вы должны создать класс \App\Infrastructure\Doctrine\DoctrineSpecificationArticleRepository для работы со спецификациями.
Вот об этих двух репозиториях на инфраструктурном слое я говорю.


Особенно это актуально если вы хотите в репозиторий добавить какие-то методы которых вам не хватает в основном репозитории. Для меня это countOf().


Да. Мы можем изменить базовый репозиторий для Doctrine. И даже можем создать свой базовый репозиторий наследующийся от EntitySpecificationRepository и добавить в него нужные нам методы. А что если нам нужно добавить методы не в общий репозиторий и не в репозиторий DoctrineArticleRepository реализующий доменный интерфейс, а нужно создать именно DoctrineSpecificationArticleRepository? Мы получим два репозитория на инфраструктурном слое.

Мы здесь обсуждаем Doctrine Specification.

это деталь реализации моих репозиториев.


Это значит что вы должны создать класс

а если тебя в ридми попросят с моста прыгнуть? вот в документации по Symfony предлагают в сущности доктрины сеттеры фичачить и забить на принцип information expert от слова совсем. Зачем они так делают? потому что иначе примеры с формами будут слишком сложные и это осознанное упрощение нацеленное на людей которым надо вув-эффект а не которые осознают что и зачем они делают.


И даже можем создать свой базовый репозиторий наследующийся от EntitySpecificationRepository и добавить в него нужные нам методы.

нас интересовать должно не это, а information hiding. Мы не добавлять должны методы а изолировать текущее. Все методы "инфраструктурные" должны быть приватными и изолированы красивым интерфейсом исключающим "неправильное использование".


Мы получим два репозитория на инфраструктурном слое.

неверно, мы получим один репозиторий в инфраструктурном слое который использует "нечто" именующееся "репозиториями" в контексте доктрины. Это разные штуки, хоть имя может тебя смущать.

нас интересовать должно не это, а 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 для получения репозитория не всегда хорошо, а иногда и плохо, но это лучше чем возможность получить через него репозиторий который вскрывает все кишки наружу, а не репозиторий реализующий интерфейс доменного слоя.


Сами сказали:


красивым интерфейсом исключающим "неправильное использование".
А вы не думали что доктрина может возвращать вам репозиторий реализацующий интерфейс доменного слоя?

вот только:


$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. Вообще никакой изоляции.

что меня не устраивает от слова совсем.

Ну это ваши личные трудности. Меня тоже много чего не устраивает и что с того?


только через setter injection, что опять же меня не устраивает.

В смысле? Использовать constructor injection религия не позволяет?


не чувствую. Information hiding надо рассматривать исключительно с точки зрения клиента

Разницы для клиента между двумя реализациями нет. Они обе дают конкретный интерфейс ArticleRepository.


Или вы намекаете что "это чудаки могут где-то заинджектить EntityManager в обход Dependency Injection?

А почему в обход? Если EntityManager вы инжектите в репозиторий, то и в любой другой сервис его можно спокойно инжектить.


Ваш же способ просто позволяет сразу всегда и везде получать инстанс EntityRepository. Вообще никакой изоляции.

Как раз наоборот. Мой вариант позволяет полностью заблокировать возможность получения EntityRepository. Полная изоляция.


Хотя я должен признаться. Я забыл что метод EntityManagerInterface::getRepository() должен возвращать ObjectRepository. Он может возвращать и другие объекты, но это будет нарушением контракта.


Так что мой вариант нарушает контракт и ваше решение правильней, хотя я считаю его неприемлемым, от слова совсем.

UFO just landed and posted this here

А что тут сложного?
Берём и реализовывает свой RepositoryFactory для доктрины. Тегетируем наши сервисы репозитории и возвращаем в фабрике.


Вот простейший пример реализации фабрики репозиториев.

А теперь приведи пример как мне сделать по фабрике на каждый репозиторий и сколько это потребует кода. И главный вопрос — зачем. Потому что тебе не нравится что у нас есть 2 вещи с названием "repository"? ну ок, логично что.

А зачем много фабрик? Одной достаточно. Кода то всего на 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);
}
А зачем много фабрик? Одной достаточно. Кода то всего на 10 строчек.

и откуда берется ids?

и откуда берется ids?

И зачем ты дурачком прикидываешся? Прекраснож понимаешь откуда.


 Тегетируем наши сервисы репозитории и возвращаем в фабрике.

Естественно ids мы получаем из Compiler Passes.
Если тебе нужны зависимости в репозитории, то ты в любом случае должен объявить их как сервисы. А чтоб можно было их получить из EntityManager, мы просто добавляем им метку.

services:
    _defaults:
        autowire: true
        public: false

    App\Domain\ArticleRepository: App\Infrastructure\Doctrine\DoctrineArticleRepository
    App\Infrastructure\Doctrine\DoctrineArticleRepository: ~

и вуаля. никаких лишних фабрик, компайл пасов, все хорошо с точки зрения разделения ответственности и соблюдения контрактов. Я все еще не понимаю почему ты считаешь такой подход "неправильным". Он требует меньше усилий со стороны разработчика, ему проще следовать, ему проще обучать. Попробуй джуну объяснить во имя чего мы переопределяли RepositoryFactory.

Всё здорово, но этого ещё нет в LTS. Со следующего года можно внедрять.


Я все еще не понимаю почему ты считаешь такой подход "неправильным".

Я уже исправился. Я уже не считаю его "неправильным". Он просто мне не нравится.

Всё здорово, но этого ещё нет в LTS.

не вопрос. Хотя могли бы просто сделать надстройку над лоадером YAML например. У меня к примеру такая надстройка есть для загрузки php конфигурации.


app.infrastructure.doctrine.article_repository:
    class: App\Infrastructure\Doctrine\DoctrineArticleRepository
    autowire: true
    public: false
    autowired_types: App\Domain\ArticleRepository

Он просто мне не нравится.

чем не нравится то?

Мне не нравится хотя бы вот это:


  • У вас 2 класса называются одинаково — repository (назови вы второй manager вопросов бы не было);
  • Они оба находятся на одном слое — инфраструктурном;
  • У них одинаковая роль — управление сущностью в хранилище (интерфейсы разные, но роль одна).

Одного этого достаточно чтоб задуматься, что что-то не так.
Потому я и предложил не разделять функции на 2 класса.


Также вы можете переименовать ваш репозиторий в manager, gateway или что-то более близкое к его роли или вы можете перенести этот репозиторий в другой слой.


И да. Я проверил. С Symfony 3.3 нам не нужно тегетируем сервис репозитория, нам не нужен компилятор для меток, нам не нужна своя фабрика репозиториев. Достаточно просто создать сервис и прописать его в аннотациях к сущности.


Свой репозиторий в Symfony 3.3

Прописываем репозиторий в аннотациях сущность


/**
 * @ORM\Table(name="article")
 * @ORM\Entity(repositoryClass=DoctrineArticleRepository")
 */
final class Article
{
    // ...
}

Реализация репозитория


class DoctrineArticleRepository
    extends EntityRepositoryDummy
    implements ArticleRepository
{
    private $em;

    private $client;

    public function __construct(
        EntityManagerInterface $em,
        ApiClient $client
    ) {
        $this->em = $em;
        $this->client = $client;
    }

    // ...
}

Для соблюдения контракта нам придется сделать заглушку


abstract class EntityRepositoryDummy implements ObjectRepository
{
    final public function find($id)
    {
        throw new \RuntimeException('This method is not implemented.');
    }

    // ...
}

И мы можем получить наш сервис репозиторий двумя способами в любом месте.


class SomeService
{
    public function __construct(
        EntityManagerInterface $em,
        ArticleRepository $repository
    ) {
        // true
        $em->getRepository(Article::class) === $repository;
    }
}

Я не предлагаю сейчас возвращаться к обсуждению целесообразности такого решения, но возможность такая есть и без единой строчки в конфиге не считая импорта.


То есть, для того чтоб перейти от вашего решения к моему, достаточно добавить extends и прописать репозиторий в аннотациях сущности.
И все. Больше ничего делать не надо.
И это гораздо проще чем настраивать deptrac.


И еще, ваши фразы


позволяет юзать не только доктрину но и скажем дергать внешние API, просто инджектишь не EM а Guzzle\Client например.

и


В этом случае у вас по сути "репозиторий" на каждую выборку (что мы выше и обсуждали) что в целом меня устраивает и я даже так иногда делаю.

наводят меня на мысли, что ваши репозитории делают больше чем должны.


PS: Если вам не нравятся лишние методы в ObjectRepository, то вы можете сделать PR или fork.

У вас 2 класса называются одинаково

так второй класс это не мой, это класс доктрины, он в vendors.


У них одинаковая роль — управление сущностью в хранилище

не совсем так. У них разная роль. У одного — управление конкретной сущностью, а у второй — общие вопросы управления сущностями. То есть у всех сущностей есть один общий репозиторий и мы "закрываем" его сверху нашим конкретным. В проекте всеравно мы не будем использовать конкретную реализацию а будем ссылаться на интерфейсы.


Также вы можете переименовать ваш репозиторий в manager, gateway или что-то более близкое к его роли

repository это максимально близкое к роли. *Manager например — это вообще антипаттерн в большинстве случаев. *Registry — может быть но это таки Repository. Причем сам интерфейс у меня будет вообще без каких-либо суффиксов — что-то типа Catalog это интерфейс для DoctrineProductRepository.


наводят меня на мысли, что ваши репозитории делают больше чем должны.

а что они должны делать? мой репозиторий выглядит так:


interface Catalog
{
     public function add(Product $product): void;
     public function getProduct(int $id): Product;
     public function find(ProductSpecification $spec);
}

Все строго согласно SOLID.


PS: Если вам не нравятся лишние методы в ObjectRepository, то вы можете сделать PR или fork.

и супортить самому… ну да ну да...

не совсем так

То есть, вы признаете что сходство ролей очевидно?
Я не предлагаю что-то менять. Хотите, называйте Repository, хотите Catalog. Это ваше право.


мой репозиторий выглядит так

Мне сложно представить зачем вам может быть нужен одновременно и ApiClient и EntityManager в одном таком репозитории.


Разве что у вас разделены read и write хранилища и вы в одном репозитории читаете из одного хранилища, а пишете в другое. Хотя это явно неправильно.


Всё что мне приходит в голову делается через доменные события.
Расскажите, зачем вам ApiClient и EntityManager в одном репозитории? Мне просто любопытно.

Мне сложно представить зачем вам может быть нужен одновременно и

а я где-то говорил что мне в ОДНОМ хранилище может понадобиться и то и то? Если мы посмотрим что именно я писал то:


инджектишь НЕ Entity Manager а Guzzle\Client например.

То есть, вы признаете что сходство ролей очевидно?

сходство не означает что это одна и та же роль. Более того, если мы говорим про SRP то смотреть надо не столько на роль сколько на "причины для изменений" и они совершенно различны.

инджектишь НЕ Entity Manager а Guzzle\Client например.

Точно. Что-то я не так прочитал. Тогда все логично)

Мне сложно представить зачем вам может быть нужен одновременно и ApiClient и EntityManager в одном таком репозитории.

Чисто в теории, репозиторий может хранить корень агрегата в СУБД, а его листья где-то удаленно. На практике вполне может быть вариант, когда репозиторий отдаёт статьи из базы, а в $article->Author подставляет значения из http-сервиса пользователей

Ну это ваши личные трудности. Меня тоже много чего не устраивает и что с того?

Если меня что-то не устраивает я ищу решение которое меня устраивает. И в 99% нахожу его. Есть 1% где надо идти на компромис но это не тот случай.


В смысле? Использовать constructor injection религия не позволяет?

напомню, что просто использовать constructor injection не выйдет. Для этого нужно:


  • сделать свою фабрику репозиториев которая будет уметь constructor injection
  • соблюсти интерфейс ObjectRepository
  • зарегистрировать отдельный сервис с использованием именно этой фабрики
  • явно задать репозиторий для сущности

мне проще сделать отдельный сервис. Это решает вообще все мои проблемы без дополнительных телодвижений.


Разницы для клиента между двумя реализациями нет. Они обе дают конкретный интерфейс ArticleRepository.

именно, и мой вариант дает для клиентского кода все то же самое и с точки зрения реализации намного проще. Так зачем платить больше?


А почему в обход? Если EntityManager вы инжектите в репозиторий, то и в любой другой сервис его можно спокойно инжектить.

повторюсь — это легко выявляется на этапе код ревью или можно даже взять любой статический анализатор зависимостей. Не вижу тут проблем.


Как раз наоборот. Мой вариант позволяет полностью заблокировать возможность получения EntityRepository. Полная изоляция.

вообще-то нет, любой может получить ObjectRepository как и раньше. Ты же должен реализовать этот интерфейс у своих репозиториев.


Так что мой вариант нарушает контракт и ваше решение правильней, хотя я считаю его неприемлемым, от слова совсем.

потому что два раза слово репозиторий фигурирует или что? Что это за религия?

Хотя мы можем добавить интерфейс ObjectRepository к DoctrineArticleRepository и не добавлять его к ArticleRepository и таки образом не будем нарушать контракт, но смысла в этом особого нет, так как создаст ненужные, скрытые, пустые методы в репозитории.

Хотя мы можем добавить интерфейс ObjectRepository к

что будет полностью нарушать основную идею изоляции. Мы НЕ хотим давать доступ пользователю нашего интерфейса ArticleRepository к ObjectRepository. От слова совсем. Вы же предлагаете сделать именно это.


Извини, но это неверный подход.

Эээ… Вы читать не умеете? Я уже тысячу раз сказал, что я не хочу давать пользователю интерфейс ObjectRepository. На каком ещё языке мне это сказать чтоб вы меня поняли? Может на белорусском?

вот только именно это ты и предлагаешь. getRepository обязан вернуть ObjectRepository. Именно это меня и удивляет.


p.s. рассмотри вариант использования deptrac на проекте для управления зависимостями и не создавай проблем на пустом месте.

ArticleRepository не на инфраструктурном уровне, а на уровне домена. На инфраструктурном DoctrineArticleRepository, MongoArticleRepository, HttpArticleRepository и т. п.

Ну так, Fesor совершенно четко написал \App\Infrastructure\Doctrine\DoctrineArticleRepository. То есть это репозиторий на инфраструктурном уровне.
Про ArticleRepository я ничего не говорил. Понятное дело что он на доменном слое.

А где второй репозиторий на инфраструктном уровне? $this->repo = $em->getRepository(Article::class);? Так это даже не просто деталь реализации, а просто оптимизация вызова метода менеджера.

Да вы батенька, мастер извращений.

репозиторий всегда инфраструктурный слой. Реализация. А вот интерфейс оного — слой бизнес логики. Магия инверсии зависимостей. За счет этого у нас уже инфраструктура зависит от бизнес логики а не наборот.


Более того, подход который я описал:


  • упрощает конфигурацию DI
  • позволяет реализовать именно тот интерфейс который нужен
  • прячет "доктрину" как деталь реализации инфраструктуры
  • позволяет юзать не только доктрину но и скажем дергать внешние API, просто инджектишь не EM а Guzzle\Client например.

Или ты хочешь предложить мне размазывать доктрину или внешние зависимости по всему проекту?

Вендор-специфичная вещь. Её следует помещать в public function findBySpecification(SpecificationInterface $spec)

как раз и есть зло. Готов поспорить что один из следующих методов будет типа

так никто подобного и не предлагает. Как раз наоборот, все знания о том как происходит фильтрация выносится в спецификацию а репозиторий теперь просто просит спецификацию (а точнее адаптер под нашу СУБД) сгенерить запрос. Инфраструктура и бизнес логика разделены.


CQRS и будет вам счастье ;) Если есть интерес, могу подробнее

CQRS никак не решает эту проблему само по себе. Если вы про CQS то да, простое разделение на "чтение" и "запись" уже дают неплохой профит в плане разделения ответственности, а вот CQRS это чуть другая история со своими плюсами и минусами.

тогда не стоило показывать в примере метод жестко запиленный под один UseCase.
само наименование метода фильтрации уже говорит о том, как в системе связаны сущности между собой.


findAvailableRegionsByDeveloper

отношения такого рода диктуются бизнесом. если у вас завтра бизнес отменит связку Developer->Region, что вы будете делать?
И вот как раз CQRS помогает держать логику бизнес-процесса и его отработку в точно определенном месте (Command/Query-Handler). Если меняется процесс, то меняется только его обработка, но никак не хранилище данных.
Опят таки, как вы решите проблему фильтрации сугубо на уровне репозитория, если ваши данные хранятся в разном виде одновременно? Регионы в базе, а девелоперы, например, в виде почтовых аккаунтов.

Если меняется процесс, то меняется только его обработка, но никак не хранилище данных.

это особенность event sourcing нежели CQRS. В этом ключе у нас есть ивенты по которым мы всегда можем построить проекцию данных под задачу. CQRS можно строить и без event sourcing, а вот последнее без первого уже проблематично.


Опят таки, как вы решите проблему фильтрации сугубо на уровне репозитория, если ваши данные хранятся в разном виде одновременно?

давайте рассуждать. Зачем нам нужны репозитории и что они должны возвращать? Эта штука которая скрывает persistence layer от слоя бизнес логики и позволяет развернуть зависимость что бы наша бизнес логика вообще не зависела от инфраструктуры.


Соответственно что могут возвращать репозитории — они должны возвращать сущность, или же VO с какой-то статистикой по всей коллекции сущностей. То есть репозиторий не должен нам возвращать коллекции, только что-то в единичном экземпляре.


Когда нам нужно получить коллекцию сущностей вне другой сущности? Обычно это связано с отображением всякого рода списочков, поисковых выборок и т.д. На этот вид задач очень похожи репорты. Запихивать логику формирования репорта который включает различные сущности в какой-то репозиторий — как минимум некорректно. Точно так же пихать выборку по разным сущностям (что бы список составить) в репозиторий какой-то одной из них не очень корректно. По хорошему репозитории мы должны делать на корень агрегата, а не на списочек.


А потому мы можем сделать что-то типа read-model, которой не нужны репозитории поскольку оно read-only. И для проблемы озвученной вами мы можем делать свои проекции этих данных для более удобной работы. Это все еще и ни CQRS и ни event sourcing. Это больше походит на принцип CQS Мэйерса.

Event 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


То есть репозиторий и есть тот самый списочек


То есть репозиторий не должен нам возвращать коллекции, только что-то в единичном экземпляре.

Это с какого перепугу?

Почему? Два концепта описывающие совершенно разные части системы.

Я Грэгу Янгу больше в этих вопросах доверяю. Как никак первоисточник. ES без CQRS невозможно. И CQRS можно делать без command/query bus и прочих вещей. Опять же отсылка к первоисточникам — Бертранд Мэйер и его CQS.


Это с какого перепугу?

Если мы говорим про репозитории в контексте бизнес логики — потому что они нужны нам что бы корень агрегата собрать. А дальше сама сущность со всем справится.

Я Грэгу Янгу больше в этих вопросах доверяю
ES без CQRS невозможно.

Уже не первый раз слышу. Можно пожалуйста ссылку, где первоисточник подтверждает ваш тезис?
Кстати, предисловие к книге на MSDN написал как раз Грэг Янг.
https://msdn.microsoft.com/en-us/library/jj591564.aspx


что бы корень агрегата собрать.

Вообще-то определение репозитория как паттерна ничего о корнях и агрегатах не говорит. То о чем вы говорите не соответствует определению репозитория. Ну или вы его зачем то дико урезаете, пытаясь его использовать. Тут конечно хозяин — барин.

Вообще-то определение репозитория как паттерна ничего о корнях и агрегатах не говорит.

Если в домене есть агрегаты с корнем и "листьями", то репозиторий должен быть только для корня по определению агрегата. Если мы делаем репозиторий для "листьев", то нарушаем принцип агрегата.

да, но вы именно CQRS предлагает как решение всех проблем, а не конкретно event sourcing.

  1. не всех проблем, а той о которой конкретно речь шла. а речь шла о том, что ИМХО в приведенном примере бизнес-логика была частично вынесена в репозиторий.
  2. с чего вы взяли, что я предлагаю ЕС как решение проблемы?
  3. последнее мое замечание было вообще о вашем пресловутом первоисточнике который сам себя таковым не считает

похоже, вам не то что матчасть учить, вам азы чтения подтянуть не мешало бы.

Абсолютно согласен.


При этом единственное, что вам нужно в репозитории — фильтрация сущностей.

то есть как раз ваш идеал и реализует то, что я сказал. а вот методы типа


findAvailableRegionsByDeveloper

это уже в сторону бизнес-логики

это уже в сторону бизнес-логики

если это что-то возвращает нам коллекцию — то нет. 90% что это нужно только для репрезентации данных (read model если хотите), и еще 10% что мы неверно выбрали корень агрегата где-то.

а если ничего не найдем и решили возвращать null вместо пустой коллекции? при чем тут коллекция? само присутствие метода с таким именем в репозитории уже проблема или по крайней мере симптом.
Ваш read model должен тогда быть примерно таким


AvailableRegionsByDeveloper {

    public function __constructor(Developer $dev) 
    {
    }

    public function all() {
        ...
    }
}
а если ничего не найдем и решили возвращать null вместо пустой коллекции?

тогда я бы отправил человека почитать о профите null-object-ов


при чем тут коллекция?

я мне кажется достаточно точно описал причем тут коллекции и как они показывают симптомы наших репозиториев.


Ваш read model должен тогда быть примерно таким

да, что-то типа такого. Но если подумать, то почти все эти модели будут содержать очень похожие по смыслу вещи, а от того проще будет сделать точку входа и кидать туда объекты которые уже занимаются описанием выборки. И это именно то чем являются спецификации.


А проблему с разграничением зон ответственностей, проекциями и т.д. мы решаем уже на чуть другом уровне.

Почитайте еще раз определение самого паттерна


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

https://martinfowler.com/eaaCatalog/repository.html

Там где есть полный список, есть и частичный.

В этом случае у вас по сути "репозиторий" на каждую выборку (что мы выше и обсуждали) что в целом меня устраивает и я даже так иногда делаю.

В этом случае у вас по сути "репозиторий" на каждую выборку

ну, вот откуда такой вывод? хватает метода для фильтрации по критериям или спецификации заданной извне.


но вообще заметил, что у вас уже сложилось свое представление о мире сием. посему, удачи вам в ваших начинаниях. и огромное спасибо, потому как такие догматики как вы обеспечиваете мне хорошо оплачиваемую работу на много лет вперед. так что Пилите, Шура! Пилите!

тогда я бы отправил человека почитать о профите null-object-ов

ну, тут не суть в null. да, я знаю, что Tony Hoare позже раскаивался :) будем выдавать false. ;)

будем выдавать false. ;)

всеравно null-object лучше, упрощает контракт.

Sign up to leave a comment.

Articles