Specification pattern

Обсуждаем, как правильно строить приложения
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Specification pattern

Сообщение sda »

Кто может объяснить, как используют Specification pattern совместно с Repository pattern? В интернете пишут, что с помощью него можно убрать из репозитория все методы такого характера UserRepository::findByFirstName($firstName), UserRepository::findByLastName($lastName), UserRepository::findByFirstAndLastName($firstName, $lastName) и оставить вместо них только UserRepository::findAll(ISpecification $specification).

Но это же получается, что я должен сначала выгрузить все данные из базы, даже если там 1 млн. строк на основе этих данных собрать 1 млн. сущностей и только потом отфильтровать и вернуть из них те, которые удовлетворяют спецификации. Даже если данные выбирать с помощью batch то есть небольшими порциями, чтобы не уронить память сервера, обрабатывать и выбирать дальше, то всё равно это как-то странно для того, чтобы найти нужные сущности вытряхивать на запрос все данные, что есть в базе. Вот пример паттерна на php https://github.com/mbrevda/SpecificationPattern но я же не хочу загружать 1 млрд. инвойсов, что есть в базе, чтобы отобрать из них те, что удовлетворяют спецификации. Или как это работает?
Аватара пользователя
samdark
Администратор
Сообщения: 9489
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: Specification pattern

Сообщение samdark »

Наверняка имеется ввиду что-то вроде нашего query builder из Yii 1.1. То есть отдельно формируется CDbCriteria и передаётся в метод findAll(), а там уже внутри на основе критерии формируется запрос.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

да, CDbCriteria это по сути реализация спецификации, только со стороны базы, а не хранилища.

Я вижу так:

Код: Выделить всё

// нам нужны посты с именем 'Новости отпуска' и с датой, ранее '2015-05-12'
$specification = (new PostSpecification)
    ->nameEqual('Новости отпуска')
    ->dateEarlierThan(new Date('2015-05-12'));

$postRepository->find($specification);

// PostRepository
public function find(PostSpecification $specification)
{
    $sql = (new SqlSpecificationTranslator)->toQuery($specification); // адаптер спецификации в запрос sql
    
    return $this->sqlConnection->select($sql);
}

// SqlSpecificationTranslator
public function toQuery(PostSpecification  $specification)
{
    $queryBuilder = new QueryBuilder;

    if($specification->getName()) {
        $queryBuilder->andWhere(['name' => $specification->getName()); // $specification->getName() на самом деле вернул EqualCondition
    }
    
    if($specification->getDate()) {
        $queryBuilder
            ->andWhere(['date > :date']) // $specification->getDate() на самом деле вернул CompareCondition
            ->bind(':date', $specification->getDate());
    }

    return $queryBuilder->getSql();
}
 
реализация спецификации может быть разной. Мы можем написать как всеохватывающую библиотеку, покрывающую различного типа запросы, либо под каждый репозиторий писать свою маленькую спецификацию. Мы можем сделать либу в виде билдера с удобным языком составления спецификации или аскетичный класс с передачей параметром в конструктор.
Но вместе со спецификацией мы сразу должны писать транслятор спецификации в язык запросов конкретного репозитория, будь то sql, http-запросы итд.

PS В моем примере QueryBuilder не делает запрос в БД как в yii, а строит непосредственно sql-выражение в виде строки, т.е. возвращает sql + забинденные параметры (опустил в примере).
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

или какая-то абстрактная спецификация:

Код: Выделить всё

$specification = new CollectionSpecification([
    new EqualCondition(['id' => 5, 'status' => Post::STATUS_PUBLISHED]),
]);

Код: Выделить всё

$specification = new CollectionSpecification([    
    new PageCondition(2, 15), // 2-я страница по 15 
    new EqualCondition(['status' => Post::STATUS_PUBLISHED]),
    new SignCondition(['date' , '>', '2015-01-01']),
    new LikeCondition(['name' , 'отпуск']),
]);

Код: Выделить всё

$specification = (new SpecificationBuilder)
    ->page(2, 15)
    ->equal(['status' => Post::STATUS_PUBLISHED])
    ->sign(['date' , '>', '2015-01-01'])
    ->like(['name' , 'отпуск']);
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Specification pattern

Сообщение sda »

Ну вот у Фаулера в его книге Patterns of Enterprise Application Architecture на странице 275 я нашел описание Query object и Criteria, он запихал в эту Criteria метод Criteria::generateSql() который и генерирует часть sql запроса. Затем на странице 283-284 он рассказывает про репозиторий и там есть примеры создания двух стратегий RelationalStrategy и InMemoryStrategy, одна ищет по реляционной базе, а другая ищет в ОЗУ. Так вот та стратегия, что ищет в ОЗУ использует в качестве Criteria тот самый Specification pattern в том виде, в каком он описан в вики с его isSatisfiedBy() методом, который ищет исключительно уже по готовым доменным объектам. Ну а RelationalStrategy судя по всему использует Criteria с generateSql() методом. И в книге это похоже на один и тот же Criteria объект.

Так вот я не понимаю до конца мысли Фаулера, я должен создавать Criteria под разные хранилища или как? Вот исходя из примеров в книге Фаулера каждая его Criteria связана с реляционной бд. Вот беру я документоориентированную бд и какие мои действия тогда должны быть по Фаулеру? А если я хочу еще и в памяти уметь хранить для тестов, тогда какие? Под каждое хранилище свой набор Criteria ? Вообще ничего не понятно...
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Specification pattern

Сообщение sda »

Я нашел, что многие делают метод типа generateSql как в книге Фаулера для Specification pattern например вот http://marcaube.ca/2015/05/specificatio ... ith-a-spec и автор тоже поднимает проблему, когда в базе много данных. Но предлагает в качестве компромисса дать протечь инфраструктуре в доменный слой или использовать inversion of control и как я понял автор этой статьи предлагает примерно такое решение http://stackoverflow.com/a/33341503/2868530

Но я тогда не пойму, зачем это всё если double dispatch точно также требует эти кастомные методы в репозитории для выборки из базы от которых мы изначально и хотели избавиться. Зачем нужен такой Specification pattern?
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Specification pattern

Сообщение slavcodev »

Обратите внимание что Спецификация это часть домена, это часть Ubiquitous Language. Так что варианты типа Query Buidler не очень подходят

Код: Выделить всё

->page(2, 15)
->equal(['status' => Post::STATUS_PUBLISHED])
->sign(['date' , '>', '2015-01-01'])
->like(['name' , 'отпуск']);
 
не очень хороший пример имхо. Когда один два сервиса юзается это сойдет (хотя ту и ActiveRecord сойдет :D) но на больших количествах сервисов, такое вот использование будет проблематичным. Это ничем не отличается от использования SQL buildera в сервисах или Yii scopes, слишком много деталей.

Мне кажется Эванс под Спецификациями имел виду уточняющие условия для группы сущностей, уже полученных из БД.

Код: Выделить всё

$posts = $repository->getAll(); 
// или более детальная выборка
$posts = $repository->getPublishedPosts();

// и когда уже есть группа нужных сущностей,
// но выделать и еще какую-то группу, то вводится спецификации
$popularPosts = array_filter($posts, new FiveMostPopularPosts(/* $viewCount > 1000 */));
$this->pinToSidebar($popularPosts);
 
Т.е. Эванс подразумевал фильтрацию уже восстановленных сущностей, а не запросы в БД, для уточняющих запросов в БД есть репозитории и его методы.

Конечно же соблазн огромный, не делать детальные репозитории, а делать генерик методы типа $repository->getBySpecification($spec),
в этом случае я видел толко одну реализацию этого дела "Double Dispatch", когда класс спецификации имплементирует два интерфейса, один доменный другой инфраструктырный. Это подход, кроме того что мешает имплементацию двух слоев, нарушает SRP, еще он нарушает, что часто упускается, LSP. Но это работает, и в отсутствии других вариантов (по крайней мере я других еще не видел), так что как говорит дядюшка Боб, SOLID можно нарушать если это делается осознано для решения каких-то задач.
Жду Yii 3!
sda
Сообщения: 334
Зарегистрирован: 2013.12.19, 09:29

Re: Specification pattern

Сообщение sda »

slavcodev у меня точно такое же восприятие этого паттерна как и у вас. Но я все равно не понимаю как сделать $repository->getBySpecification($spec) и при этом избавиться от детальных методов с помощью double dispatch, ведь детальные методы в этом случае все равно остаются в репозитории, просто теперь эти методы вызываются внутри спецификации. У меня выше ссылка на стековерфлоу где есть примеры, которые это демонстрируют. Поэтому я не понимаю, что это дает и зачем так делать, если это не дает никаких преимуществ перед тем как если эти детальные методы вызывать сразу в application layer без всяких спецификаций.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

slavcodev писал(а):Обратите внимание что Спецификация это часть домена, это часть Ubiquitous Language. Так что варианты типа Query Buidler не очень подходят

Код: Выделить всё

->page(2, 15)
->equal(['status' => Post::STATUS_PUBLISHED])
->sign(['date' , '>', '2015-01-01'])
->like(['name' , 'отпуск']);
 
не очень хороший пример имхо. Когда один два сервиса юзается это сойдет (хотя ту и ActiveRecord сойдет :D) но на больших количествах сервисов, такое вот использование будет проблематичным. Это ничем не отличается от использования SQL buildera в сервисах или Yii scopes, слишком много деталей.
поясни мысль.
slavcodev писал(а):Мне кажется Эванс под Спецификациями имел виду уточняющие условия для группы сущностей, уже полученных из БД.

Код: Выделить всё

$posts = $repository->getAll(); 
// или более детальная выборка
$posts = $repository->getPublishedPosts();

// и когда уже есть группа нужных сущностей,
// но выделать и еще какую-то группу, то вводится спецификации
$popularPosts = array_filter($posts, new FiveMostPopularPosts(/* $viewCount > 1000 */));
$this->pinToSidebar($popularPosts);
 
Т.е. Эванс подразумевал фильтрацию уже восстановленных сущностей, а не запросы в БД, для уточняющих запросов в БД есть репозитории и его методы.
про Эванса ясно, но в таком виде спецификация мало пригодна для работы. Поэтому например Фаулер ее развил до generateSql() (в книге DDD in PHP насколько помню тоже специфкация используется в таком же виде)
slavcodev писал(а):

Код: Выделить всё

Конечно же соблазн огромный, не делать детальные репозитории, а делать генерик методы типа $repository->getBySpecification($spec),
в этом случае я видел толко одну реализацию этого дела "Double Dispatch", когда класс спецификации имплементирует два интерфейса, один доменный другой инфраструктырный. Это подход, кроме того что мешает имплементацию двух слоев, нарушает SRP, еще он нарушает, что часто упускается, LSP. Но это работает, и в отсутствии других вариантов (по крайней мере я других еще не видел), так что как говорит дядюшка Боб, SOLID можно нарушать если это делается осознано для решения каких-то задач.[/quote]я не стал разбираться в приведенных sda ссылок, но я предложил вариант без протечек и double dispatch.

У нас есть в домене сущность, репозиторий с generic методом findBySpec(...) и спецификации запросов в репозиторий. Реализуя интерфейс репозитория в инфраструктуре мы также реализуем трансляцию модели спецификации в нативный язык запросов для реализации репозитория.
Аватара пользователя
samdark
Администратор
Сообщения: 9489
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: Specification pattern

Сообщение samdark »

Вообще это палка о двух концах. Мы избавляемся от тучи конкретных методов, но взамен получаем тучу раздутого кода по формированию спецификации. Не от похожего ли кода мы избавлялись, вводя репозитории?
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

Sam Dark писал(а):Вообще это палка о двух концах. Мы избавляемся от тучи конкретных методов, но взамен получаем тучу раздутого кода по формированию спецификации. Не от похожего ли кода мы избавлялись, вводя репозитории?
репозитории мы вводили для абстракции от хранилища.
на реальном проекте (не из книги) мы сразу же столкнемся с тем, что репозитории должны подерживать пагинацию. плюс через 2-3 года разработки репозитории могут раздуться до 50 методов, поддерживающих различные типа выборок. Спецификация поможет сохранить чистоту.
Sam Dark писал(а):Мы избавляемся от тучи конкретных методов, но взамен получаем тучу раздутого кода по формированию спецификации.
ну как мне представляется, в случае с билдером спецификаций общего вида (в виде библиотеки) нам понадобится реализовать универсальный транслятор строк в 50 и все.

Возможно не стоило называть предложенное мной решение спецификацией - скорее это QueryObject, что впрочем не отменяет его предназначения.

https://github.com/dddinphp/book-issues ... -198759985 вот примерно об этом я и говорю
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Specification pattern

Сообщение slavcodev »

И того же мнения. Спецификации, как фильтры сущностей, удобны, избавляют от кучи foreach, в боготом логикой домене. А вот генерация SQL в спецификациях, может быть интересна только как какой-то повторяющий код. Так что Double Dispatch мне не нравится, и я экспериментирую с другим решением.
Жду Yii 3!
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Specification pattern

Сообщение slavcodev »

zelenin писал(а):плюс через 2-3 года разработки репозитории могут раздуться до 50 методов, поддерживающих различные типа выборок. Спецификация поможет сохранить чистоту.
Чем плохи репозитории с 50 методов? Все хранится в одном классе, чей смысл это как список сущностей? Очень даже хорошо получается все контролировать. Имплеентацию этих 50 методов, можно сделать использую query buidler + scopes какие-нибудь, и 50 методов буду выгялдит красиво и чисто. Но за то вся логика по фильтрации и группировке сущностей в одном классе ответственность которого как раз в этом.
Жду Yii 3!
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

slavcodev писал(а):И того же мнения. Спецификации, как фильтры сущностей, удобны, избавляют от кучи foreach, в боготом логикой домене. А вот генерация SQL в спецификациях, может быть интересна только как какой-то повторяющий код. Так что Double Dispatch мне не нравится, и я экспериментирую с другим решением.
я предложил генерить sql репозиторию на основе переданной спеки. Сама спека ничего не генерит, а только представляет информацию о запросе в репо.
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: Specification pattern

Сообщение zelenin »

slavcodev писал(а):
zelenin писал(а):плюс через 2-3 года разработки репозитории могут раздуться до 50 методов, поддерживающих различные типа выборок. Спецификация поможет сохранить чистоту.
Чем плохи репозитории с 50 методов? Все хранится в одном классе, чей смысл это как список сущностей? Очень даже хорошо получается все контролировать. Имплеентацию этих 50 методов, можно сделать использую query buidler + scopes какие-нибудь, и 50 методов буду выгялдит красиво и чисто.
50 методов не отменяют наличия более общего метода. а scope + пагинация - это и есть вариация QueryObject.
slavcodev писал(а): Но за то вся логика по фильтрации и группировке сущностей в одном классе ответственность которого как раз в этом.
так я и не говорю куда-то выносить фильтрацию/группировку.
Если у нас в домене есть Repository+QueryObject, то реализация репозитория должна понимать этот QueryObject, преобразуя в свой нативный язык (sql ли это будет либо http-запросы к апи).
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Specification pattern

Сообщение slavcodev »

Да именно похожее решение я эксперементально интегрирую сейчас в два баундед контекста, один CRUD другой CQRS, посмотрим во что это превратится :D.
Жду Yii 3!
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

uery

Сообщение slavcodev »

zelenin писал(а):так я и не говорю куда-то выносить фильтрацию/группировку.
Если у нас в домене есть Repository+QueryObject, то реализация репозитория должна понимать этот QueryObject, преобразуя в свой нативный язык (sql ли это будет либо http-запросы к апи).
Использование QueryBuilder, имхо, уходит далеко за пределы UL, и уменьшает всю красоту DDD. QueryBuilder это уже какой-то технический прием программиста, чтоб облегчить себе жизнь а не DDD.

Код: Выделить всё

class MyService
{
  public function handle()
  {
    $query = new QueryBuilder();
    $query->page(2, 15)
      ->equal(['status' => Post::STATUS_PUBLISHED])
      ->sign(['date' , '>', '2015-01-01'])
      ->like(['name' , 'отпуск']);

    $posts = $repository->getPostsByQuery($query);
  }
}
WUT???? Что за query? Что что значит все слова из цепочки? Кто их о них рассказал? Доменный эксперт? Он думаете знает что такое билдер, не думает ли он что это строитель домов?
Он так и сказал: "Хочу иметь билдер, с кучей настроек, и искать посты по этим значениям"? Сомневаюсь.

Он попросит Вас:

- "Найти все посты разбитые на страницы"
- "Найти новые посты разбитые на страницы"
- "Найти посты по заданной строке разбитые на страницы"

50 методов в репозитории? Тоже сомневаюсь, а если со временем и появится, то скорее всего прийдет время рефакторинга и пересмотра UL, может быть эти методы можно объединить, переопределить.
Жду Yii 3!
zelenin
Сообщения: 10596
Зарегистрирован: 2013.04.20, 11:30

Re: uery

Сообщение zelenin »

slavcodev писал(а):
zelenin писал(а):так я и не говорю куда-то выносить фильтрацию/группировку.
Если у нас в домене есть Repository+QueryObject, то реализация репозитория должна понимать этот QueryObject, преобразуя в свой нативный язык (sql ли это будет либо http-запросы к апи).
Использование QueryBuilder, имхо, уходит далеко за пределы UL, и уменьшает всю красоту DDD. QueryBuilder это уже какой-то технический прием программиста, чтоб облегчить себе жизнь а не DDD.

Код: Выделить всё

class MyService
{
  public function handle()
  {
    $query = new QueryBuilder();
    $query->page(2, 15)
      ->equal(['status' => Post::STATUS_PUBLISHED])
      ->sign(['date' , '>', '2015-01-01'])
      ->like(['name' , 'отпуск']);

    $posts = $repository->getPostsByQuery($query);
  }
} 
WUT???? Что за query? Что что значит все слова из цепочки? Кто их о них рассказал? Доменный эксперт? Он думаете знает что такое билдер, не думает ли он что это строитель домов?
Он так и сказал: "Хочу иметь билдер, с кучей настроек, и искать посты по этим значениям"? Сомневаюсь.
это всего лишь паттерн Builder, позволяющий в более простой манере (да, для программиста) составлять QueryObject (я специально привел три варианта реализации, чтобы показать как можно более удобно реализовать обертку над спекой с UL).
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: Specification pattern

Сообщение slavcodev »

И еще, вот написал ты в N сервисах билдер типа этого

Код: Выделить всё

class MyService
{
  public function handle()
  {
    $query = new QueryBuilder();
    $query->page(2, 15)
      ->equal(['status' => Post::STATUS_PUBLISHED])
      ->sign(['date' , '>', '2015-01-01'])
      ->like(['name' , 'отпуск']);

    $posts = $repository->getPostsByQuery($query);
  }
} 
И поменялся у тебя домен, и нет больше "name" атрибута, есть "firstName" + "lastName", вот весело будет. Всего-то надо было в репозиторий добавить

Код: Выделить всё

public function getPostsBySearchString($q) : array;
И в нем рулить какие поля можно делать like а по каким нет.
Жду Yii 3!
Аватара пользователя
slavcodev
Сообщения: 3134
Зарегистрирован: 2009.04.02, 21:42
Откуда: Valencia
Контактная информация:

Re: uery

Сообщение slavcodev »

zelenin писал(а):я специально привел три варианта реализации, чтобы показать как можно более удобно реализовать обертку над спекой с UL.
Я обсуждаю только вариант с билдером, он никак не относится к UL имхо.
Жду Yii 3!
Ответить