Я постараюсь максимально коротко рассказать о том, как можно использовать этот паттерн с нашей любимой Doctrine на примерах и почему так делать — true.

Давайте представим себе базовый кейс:

1. У нас есть: сущность «Дом», сущность «Квартира в доме», сущность «Застройщик», сущность «Регион».
2. У нас есть задача: иметь возможность получить всех застройщиков, иметь возможность получить все занятые регионы застройщиком, уметь возможность получить все дома, которые принадлежат застройщику и все доступные регионы вообще в принципе, где ведутся продажи домов.
3. У нас есть правила от бизнеса:

Валидный застройщик — это тот, которого мы подтвердили через админку, т.е. у которого $verifed = true.

«Но правда мы не знаем, как оно будет потом, может быть условие валидности вскоре поменяется — хз, ребята».

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

«А еще чтобы была привязанна хотя бы одна квартира, у нас пока непонятно, мы думаем дома без квартир не показывать никак нигде!1! Но тут опять же, мы можем изменить понятие валидности — пока пусть будет так. Это ведь не долго потом поправить?!!! А, кстати да!!1 Если застройщик без $verifed = true, мы должны не показывать эти дома ващеее!!! Недолго же поправить?».

«А еще мы хотим чтобы показывались только те регионы, в которых есть хотя бы 1 валидный дом!!!1 И кстати такую фильтрацию надо проворачивать как на главной странице, так и на странице отдельного застройщика!!! Ты же помнишь что такое валидный дом у нас?? А??? Ты жив??»

Итак, как раньше выглядили бы мои репозитории:

RegionRepository:
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();
    }
}


HouseRepository:
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();
    }
}


DeveloperCompanyRepository:
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
Третий шаг — прими факт того, что ты достоин лучшего и создай следующие класссы:

Специфика выборки валидных застройщиков.

CorrectDeveloperSpecification
use Happyr\DoctrineSpecification\BaseSpecification;
use Happyr\DoctrineSpecification\Spec;

class CorrectDeveloperSpecification extends BaseSpecification
{
    public function getSpec()
    {
        return Spec::eq('verified', true);
    }
}


Специфика выборки валидных домов.

CorrectHouseSpecification
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')
        );
    }
}


Специфика выборки валидных регионов.

CorrectRegionSpecification
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пецифика выборки валидных по застройщику:

CorrectOccupiedRegionByDeveloperSpecification
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())
        );
    }
}


Теперь самое приятное — уничтожаем, расщепляем, сжигаем говнокод из репозиториев! Прежде чем посмотреть под спойлеры, убедитесь, что вы ничем не отвлечены и готовы полностью вкусить тот факт, насколько же проще и божественней стал код…

RegionRepository
class RegionRepository extends EntitySpecificationRepository
{
    public function findAvailableRegions()
    {
        return $this->match(
            new CorrectRegionSpecification()
        );
    }

    public function findAvailableRegionsByDeveloper(DeveloperCompany $developerCompany)
    {
        return $this->match(
            new CorrectOccupiedRegionByDeveloperSpecification($developerCompany)
        );
    }
}


HouseRepository
class HouseRepository extends EntitySpecificationRepository
{
    public function findAvailableHouses()
    {
        return $this->match(
            new CorrectHouseSpecification()
        );
    }
}


DeveloperCompanyRepository
class DeveloperCompanyRepository extends EntitySpecificationRepository
{
    public function findAvailableDevelopers()
    {
        return $this->match(
            new CorrectDeveloperSpecification()
        );
    }
}


Ну разве не конфета?

Ссылка на бундл — тут полно описания, как можно нужно использовать спецификации.

Всем приятных часов кодинга, паттернов, солнечного утра и тишины в вашем Open Space.
Поделиться с друзьями
-->

Комментарии (81)


  1. oxidmod
    29.07.2017 18:05

    Выглядит здорово, но давайте посмотрим еще раз.

    Чем по сути является спецификация? По сути это бизнес-правила. А раз так, то и определяться они должны в слое бизнес-логики. Но вот бизнес логике нет дела до персистент-слоя. Он не должен знать что там доктрина под капотом. Может там вообще файлики или ин-мемори хранилище. А эти спецификации тащат в бизнес-логику знания о СУБД и доктрине


    1. php_freelancer
      29.07.2017 21:42

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

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

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

      Вообще, если с точки зрения DDD это не какой-то банальный кейс, я бы очень хотел услышать ваше мнение, как и что Вы бы применили для решения подобных проблем.


    1. VolCh
      30.07.2017 07:26
      +3

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

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


    1. bighoc
      31.07.2017 12:51

      Посмотрел еще раз.

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

      А вот создавать классы

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


      1. VolCh
        01.08.2017 11:46

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


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


        1. ghost404
          01.08.2017 13:31

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


          1. VolCh
            01.08.2017 13:42

            Есть DSL такой 'group in :groups and points > :min_points'; https://github.com/K-Phoen/rulerz/blob/master/doc/writing_rules.md


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


            1. ghost404
              01.08.2017 19:37

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


              1. VolCh
                02.08.2017 07:03
                +2

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


                • задаём условия каким-то универсальным способом, внутри сведенным к AST
                • пишем адаптеры по применению условий к каким-то конкретным поставщикам данным
                • чтобы каждый раз не парсить условия делаем компилятор


  1. mrsuh
    30.07.2017 09:19

    Для решения таких задач можно пользоваться фильтрами Doctrine
    http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/filters.html
    https://habrahabr.ru/post/273477


    1. php_freelancer
      30.07.2017 10:55

      Как бы вы комбинировали реализацию таких фильтров?

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

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

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


    1. Fesor
      30.07.2017 19:42
      +2

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


  1. 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]);
        }
    }
    


    1. Fesor
      30.07.2017 19:44
      +1

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


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


      1. michael_vostrikov
        30.07.2017 20:37

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


        1. Fesor
          30.07.2017 23:21

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

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


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

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


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

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


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


          1. michael_vostrikov
            31.07.2017 08:47

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

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


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

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


            1. VolCh
              01.08.2017 11:52
              +1

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

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


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


  1. Rathil
    30.07.2017 10:51
    +1

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


  1. zloyusr
    30.07.2017 11:37
    +1

    Один из авторов Doctrine 2 еще 4 года назад описывал применение Specification Pattern в контексте Doctrine 2:
    https://beberlei.de/2013/03/04/doctrine_repositories.html.


  1. ncix
    30.07.2017 19:08
    -2

    А не проще VIEW в БД написать?


    1. bighoc
      31.07.2017 11:57

      View и Doctrine не очень хорошо работают вместе


      1. VolCh
        01.08.2017 13:35

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


    1. agr_ugraweb
      31.07.2017 11:57

      так и до CQRS не далеко ))


      1. Fesor
        31.07.2017 13:29
        +1

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


        1. agr_ugraweb
          31.07.2017 13:43

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


  1. ghost404
    31.07.2017 12:50
    +1

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


    1. Fesor
      31.07.2017 13:29
      +1

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


      1. oxidmod
        31.07.2017 15:09
        +2

        На соседнем проекте ребята используют rulerZ и очень довольны


        1. Fesor
          31.07.2017 19:10
          +1

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


      1. ghost404
        31.07.2017 20:12

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


  1. boblgum
    02.08.2017 11:57

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


    public function findAvailableRegionsByDeveloper(DeveloperCompany $developerCompany)

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


    public function findAvailableRegionsByDeveloperAndSomeThingElse(DeveloperCompany $developerCompany, $somethingelse)

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


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


    1. oxidmod
      02.08.2017 12:11
      +1

      Смысл спецификаций в репо как раз в том, чтобы не плодить методы.

      В идеале будет что-то такое

      public function findBySpecification(SpecificationInterface $spec) {
          return $this->specParser->getQuery($spec)->execute();
      }
      


      1. ghost404
        03.08.2017 09:22

        Зачем findBySpecification если есть match?


        1. Fesor
          03.08.2017 10:58

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


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


            1. 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);
                   }
              }

              вуаля.


              1. ghost404
                04.08.2017 09:29

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


                1. zelenin
                  04.08.2017 10:54
                  +1

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


                  1. ghost404
                    04.08.2017 20:09
                    -1

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


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

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


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


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


                    1. Fesor
                      04.08.2017 20:17

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

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


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

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


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

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


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

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


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


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


                        красивым интерфейсом исключающим "неправильное использование".


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


                          1. ghost404
                            05.08.2017 22:51

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

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


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

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


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

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


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

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


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

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


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


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


                            1. oxidmod
                              05.08.2017 23:00

                              А нука продемонстрируйте получение репы из ентити менеджера з доп зависимостями в контрукторе


                              1. ghost404
                                05.08.2017 23:37

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


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


                                1. Fesor
                                  06.08.2017 15:57

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


                                  1. 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);
                                    }


                                    1. Fesor
                                      07.08.2017 01:37
                                      +1

                                      А зачем много фабрик? Одной достаточно. Кода то всего на 10 строчек.

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


                                      1. ghost404
                                        07.08.2017 09:58

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

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


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

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


                                        1. Fesor
                                          07.08.2017 10:28
                                          +1

                                          services:
                                              _defaults:
                                                  autowire: true
                                                  public: false
                                          
                                              App\Domain\ArticleRepository: App\Infrastructure\Doctrine\DoctrineArticleRepository
                                              App\Infrastructure\Doctrine\DoctrineArticleRepository: ~

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


                                          1. ghost404
                                            07.08.2017 20:19

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


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

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


                                            1. 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
                                              

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

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


                            1. Fesor
                              06.08.2017 16:02

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

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


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

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


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

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


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

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


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

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


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

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


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

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


                          1. ghost404
                            05.08.2017 23:11

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


                            1. Fesor
                              06.08.2017 15:56

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

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


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


                              1. ghost404
                                06.08.2017 17:03

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


                                1. Fesor
                                  07.08.2017 01:38
                                  +1

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


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


                1. VolCh
                  04.08.2017 12:58
                  +1

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


                  1. ghost404
                    04.08.2017 19:53

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


                    1. VolCh
                      04.08.2017 19:56

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


                      1. ghost404
                        04.08.2017 20:10

                        Читайте мой комментарий выше.


                1. Fesor
                  04.08.2017 20:15

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

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


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


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

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


        1. VolCh
          03.08.2017 13:57
          +1

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


    1. Fesor
      03.08.2017 00:24
      +1

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

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


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

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


      1. boblgum
        03.08.2017 09:55

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


        findAvailableRegionsByDeveloper

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


        1. Fesor
          03.08.2017 11:17
          +2

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

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


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

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


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


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


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


          1. boblgum
            03.08.2017 12:44

            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


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


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

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


            1. Fesor
              03.08.2017 18:32
              -1

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

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


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

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


              1. boblgum
                03.08.2017 19:28

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

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


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

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


                1. VolCh
                  04.08.2017 13:01

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

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


              1. 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/
                Не поленитесь, прошу, и сами проверьте, кто же это сказал.


                1. Fesor
                  03.08.2017 22:17

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


  1. boblgum
    02.08.2017 12:18

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


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

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


    findAvailableRegionsByDeveloper

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


    1. Fesor
      03.08.2017 00:25
      +1

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

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


      1. boblgum
        03.08.2017 10:06

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


        AvailableRegionsByDeveloper {
        
            public function __constructor(Developer $dev) 
            {
            }
        
            public function all() {
                ...
            }
        }


        1. Fesor
          03.08.2017 11:21

          а если ничего не найдем и решили возвращать null вместо пустой коллекции?

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


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

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


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

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


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


          1. 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

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


            1. Fesor
              03.08.2017 22:21

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

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


          1. boblgum
            03.08.2017 15:28

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

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


            1. Fesor
              03.08.2017 22:19

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

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