Пара слов о спецификациях

    Всем доброго времени суток! Удивительно, но упоминание о шаблоне "Спецификация" в контексте php встречается крайне редко. А ведь с его помощью можно не только избежать комбинаторного взрыва методов репозитория, но и улучшить переиспользование кода. Я же в свою очередь хотел бы остановиться на еще одной возможности, предоставляемой данным паттерном. С ее помощью можно решить проблему, которая возникает почти в каждом веб-приложении. И лично мне очень не хватало этого знания еще пару лет назад.


    Что будем делать


    Предположим, что мы разрабатываем task tracker. На главной странице будет выводиться список задач. Также нам понадобится просмотр отдельной задачи.


    TaskController.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Controller;
    
    use App\Entity\Task;
    use App\Repository\TaskRepository;
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\Routing\Annotation\Route;
    
    #[Route('/task')]
    final class TaskController extends AbstractController
    {
        #[Route('/', name: 'task_index', methods: ['GET'])]
        public function index(TaskRepository $taskRepository): Response
        {
            return $this->render('task/index.html.twig', [
                'tasks' => $taskRepository->findAll(),
            ]);
        }
    
        #[Route('/{id}', name: 'task_show', methods: ['GET'])]
        public function show(Task $task): Response
        {
            return $this->render('task/show.html.twig', [
                'task' => $task,
            ]);
        }
    }

    Далее предположим, что у нас есть 3 типа пользователей:


    • Admin — может работать со всеми задачами.
    • Manager — может работать только с задачами своего проекта.
    • Developer — может работать только с назначенными ему задачами.

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


    TaskController.php
    namespace App\Controller;
    
     use App\Entity\Task;
    +use App\Entity\User;
     use App\Repository\TaskRepository;
    +use App\Security\CurrentUserProvider;
    +use Doctrine\ORM\QueryBuilder;
     use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
     use Symfony\Component\HttpFoundation\Response;
    +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
     use Symfony\Component\Routing\Annotation\Route;
    +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
    
     #[Route('/task')]
     final class TaskController extends AbstractController
     {
    +    public function __construct(private AuthorizationCheckerInterface $authorizationChecker, private CurrentUserProvider $currentUserProvider)
    +    {
    +    }
    +
         #[Route('/', name: 'task_index', methods: ['GET'])]
         public function index(TaskRepository $taskRepository): Response
         {
    +        $queryBuilder = $taskRepository->createQueryBuilder('t');
    +        $this->filter($queryBuilder);
    +
             return $this->render('task/index.html.twig', [
    -            'tasks' => $taskRepository->findAll(),
    +            'tasks' => $queryBuilder->getQuery()
    +                ->getResult(),
             ]);
         }
    
    +    private function filter(QueryBuilder $queryBuilder): void
    +    {
    +        if ($this->authorizationChecker->isGranted(User::ROLE_ADMIN)) {
    +            return;
    +        }
    +
    +        $user = $this->currentUserProvider->getUser();
    +
    +        if ($this->authorizationChecker->isGranted(User::ROLE_MANAGER)) {
    +            $queryBuilder->andWhere('t.project in(:projects)')
    +                ->setParameter('projects', $user->getProjects());
    +
    +            return;
    +        }
    +
    +        $queryBuilder->andWhere('t.performedBy = :performedBy')
    +            ->setParameter('performedBy', $user);
    +    }
    +
         #[Route('/{id}', name: 'task_show', methods: ['GET'])]
         public function show(Task $task): Response
         {
    +        if (!$this->isViewable($task)) {
    +            throw new AccessDeniedHttpException();
    +        }
    +
             return $this->render('task/show.html.twig', [
                 'task' => $task,
             ]);
         }
    +
    +    private function isViewable(Task $task): bool
    +    {
    +        if ($this->authorizationChecker->isGranted(User::ROLE_ADMIN)) {
    +            return true;
    +        }
    +
    +        $user = $this->currentUserProvider->getUser();
    +
    +        if ($this->authorizationChecker->isGranted(User::ROLE_MANAGER)) {
    +            return $user->getProjects()
    +                ->contains($task->getProject());
    +        }
    +
    +        return $task->getPerformedBy() === $user;
    +    }
     }

    Конечно, писать много кода в контроллере — это не очень хорошо. Можно так или иначе раскидать его по сервисам, задействовать стандартные symfony voters. Но основная проблема этого кода в том, что наши бизнес-правила полностью повторяются и в методе filter, и в методе isViewable. И исправление этого факта уже не выглядит столь очевидно. Что можно с этим сделать? Нам нужна абстракция бизнес-правила, работающая как для списка элементов, так и для отдельной сущности. Именно это и предоставляет шаблон "Спецификация".


    Пишем Спецификацию


    В настоящий момент я нашел 2 проекта, реализующих данный паттерн для php. Happyr/Doctrine-Specification и K-Phoen/rulerz. При этом первый не поддерживает работу с отдельными объектами, а второй фактически заброшен и на symfony 5 уже не устанавливается. Да и формирование правил в строке, признаться, мне не слишком нравится.


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


    Specification.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Specification;
    
    use Doctrine\ORM\QueryBuilder;
    use Symfony\Component\PropertyAccess\PropertyAccess;
    
    abstract class Specification
    {
        abstract public function isSatisfiedBy(object $entity): bool;
    
        abstract public function generateDql(string $alias): ?string;
    
        abstract public function getParameters(): array;
    
        public function modifyQuery(QueryBuilder $queryBuilder): void
        {
        }
    
        public function filter(QueryBuilder $queryBuilder): void
        {
            $this->modifyQuery($queryBuilder);
            $alias = $queryBuilder->getRootAliases()[0];
            $dql = $this->generateDql($alias);
    
            if (null === $dql) {
                return;
            }
    
            $queryBuilder->where($dql);
    
            foreach ($this->getParameters() as $field => $value) {
                $queryBuilder->setParameter($field, $value);
            }
        }
    
        protected function getFieldValue(object $entity, string $field): mixed
        {
            return PropertyAccess::createPropertyAccessorBuilder()
                ->enableExceptionOnInvalidIndex()
                ->getPropertyAccessor()
                ->getValue($entity, $field);
        }
    }

    Помимо базовых в спецификации присутствуют вспомогательные методы. Метод filter упрощает ее применение к объекту query builder. Метод getFieldValue
    пригодится нам при создании операций.


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


    CompositeSpecification.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Specification;
    
    use Doctrine\ORM\QueryBuilder;
    
    abstract class CompositeSpecification extends Specification
    {
        abstract public function getSpecification(): Specification;
    
        public function isSatisfiedBy(object $entity): bool
        {
            return $this->getSpecification()
                ->isSatisfiedBy($entity);
        }
    
        public function generateDql(string $alias): ?string
        {
            return $this->getSpecification()
                ->generateDql($alias);
        }
    
        public function getParameters(): array
        {
            return $this->getSpecification()
                ->getParameters();
        }
    
        public function modifyQuery(QueryBuilder $queryBuilder): void
        {
            $this->getSpecification()
                ->modifyQuery($queryBuilder);
        }
    }

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


    AlwaysSpecified.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Specification;
    
    final class AlwaysSpecified extends Specification
    {
        public function isSatisfiedBy(object $entity): bool
        {
            return true;
        }
    
        public function generateDql(string $alias): ?string
        {
            return null;
        }
    
        public function getParameters(): array
        {
            return [];
        }
    }

    Equals.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Specification;
    
    final class Equals extends Specification
    {
        public function __construct(private string $field, private mixed $value)
        {
        }
    
        public function isSatisfiedBy(object $entity): bool
        {
            return $this->value === $this->getFieldValue($entity, $this->field);
        }
    
        public function generateDql(string $alias): ?string
        {
            return sprintf('%s.%s = :%2$s', $alias, $this->field);
        }
    
        public function getParameters(): array
        {
            return [
                $this->field => $this->value,
            ];
        }
    }

    MemberOf.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Specification;
    
    final class MemberOf extends Specification
    {
        public function __construct(private string $field, private object $value)
        {
        }
    
        public function isSatisfiedBy(object $entity): bool
        {
            return $this->getFieldValue($entity, $this->field)
                ->contains($this->value);
        }
    
        public function generateDql(string $alias): ?string
        {
            return sprintf(':%2$s member of %1$s.%2$s', $alias, $this->field);
        }
    
        public function getParameters(): array
        {
            return [
                $this->field => $this->value,
            ];
        }
    }

    Not.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Specification;
    
    final class Not extends Specification
    {
        public function __construct(private Specification $specification)
        {
        }
    
        public function isSatisfiedBy(object $entity): bool
        {
            return !$this->specification
                ->isSatisfiedBy($entity);
        }
    
        public function generateDql(string $alias): ?string
        {
            return sprintf(
                'not (%s)',
                $this->specification->generateDql($alias)
            );
        }
    
        public function getParameters(): array
        {
            return $this->specification
                ->getParameters();
        }
    }

    Добавлять их можно по мере необходимости. Чуть хитрее обстоит дело с объединением таблиц. Я попробовал несколько вариантов и в итоге остановился на этом.


    Join.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Specification;
    
    use Doctrine\ORM\QueryBuilder;
    
    final class Join extends Specification
    {
        public function __construct(private string $rootAlias, private string $field, private Specification $specification)
        {
        }
    
        public function isSatisfiedBy(object $entity): bool
        {
            return $this->specification
                ->isSatisfiedBy($this->getFieldValue($entity, $this->field));
        }
    
        public function generateDql(string $alias): ?string
        {
            return $this->specification
                ->generateDql($this->field);
        }
    
        public function getParameters(): array
        {
            return $this->specification
                ->getParameters();
        }
    
        public function modifyQuery(QueryBuilder $queryBuilder): void
        {
            $queryBuilder->join(sprintf('%s.%s', $this->rootAlias, $this->field), $this->field);
            $this->specification
                ->modifyQuery($queryBuilder);
        }
    }

    Переходим на бизнес-правила


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


    IsViewable.php
    <?php
    
    declare(strict_types=1);
    
    namespace App\Specification\Task;
    
    use App\Entity\User;
    use App\Security\CurrentUserProvider;
    use App\Specification\AlwaysSpecified;
    use App\Specification\CompositeSpecification;
    use App\Specification\Equals;
    use App\Specification\Join;
    use App\Specification\MemberOf;
    use App\Specification\Specification;
    use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
    
    final class IsViewable extends CompositeSpecification
    {
        public function __construct(private AuthorizationCheckerInterface $authorizationChecker, private CurrentUserProvider $currentUserProvider)
        {
        }
    
        public function getSpecification(): Specification
        {
            if ($this->authorizationChecker->isGranted(User::ROLE_ADMIN)) {
                return new AlwaysSpecified();
            }
    
            $user = $this->currentUserProvider->getUser();
    
            if ($this->authorizationChecker->isGranted(User::ROLE_MANAGER)) {
                $isProjectMember = new MemberOf('members', $user);
    
                return new Join('task', 'project', $isProjectMember);
            }
    
            return new Equals('performedBy', $user);
        }
    }

    А вот в контроллере кода поубавится.


    TaskController.php
    namespace App\Controller;
    
     use App\Entity\Task;
    -use App\Entity\User;
     use App\Repository\TaskRepository;
    -use App\Security\CurrentUserProvider;
    -use Doctrine\ORM\QueryBuilder;
    +use App\Specification\Task\IsViewable;
     use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
     use Symfony\Component\HttpFoundation\Response;
     use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
     use Symfony\Component\Routing\Annotation\Route;
    -use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
    
     #[Route('/task')]
     final class TaskController extends AbstractController
     {
    -    public function __construct(private AuthorizationCheckerInterface $authorizationChecker, private CurrentUserProvider $currentUserProvider)
    +    public function __construct(private IsViewable $isViewable)
         {
         }
    
    @@ -26,7 +23,7 @@ final class TaskController extends AbstractController
         public function index(TaskRepository $taskRepository): Response
         {
             $queryBuilder = $taskRepository->createQueryBuilder('t');
    -        $this->filter($queryBuilder);
    +        $this->isViewable->filter($queryBuilder);
    
             return $this->render('task/index.html.twig', [
                 'tasks' => $queryBuilder->getQuery()
    @@ -34,29 +31,10 @@ final class TaskController extends AbstractController
             ]);
         }
    
    -    private function filter(QueryBuilder $queryBuilder): void
    -    {
    -        if ($this->authorizationChecker->isGranted(User::ROLE_ADMIN)) {
    -            return;
    -        }
    -
    -        $user = $this->currentUserProvider->getUser();
    -
    -        if ($this->authorizationChecker->isGranted(User::ROLE_MANAGER)) {
    -            $queryBuilder->andWhere('t.project in(:projects)')
    -                ->setParameter('projects', $user->getProjects());
    -
    -            return;
    -        }
    -
    -        $queryBuilder->andWhere('t.performedBy = :performedBy')
    -            ->setParameter('performedBy', $user);
    -    }
    -
         #[Route('/{id}', name: 'task_show', methods: ['GET'])]
         public function show(Task $task): Response
         {
    -        if (!$this->isViewable($task)) {
    +        if (!$this->isViewable->isSatisfiedBy($task)) {
                 throw new AccessDeniedHttpException();
             }
    
    @@ -64,20 +42,4 @@ final class TaskController extends AbstractController
                 'task' => $task,
             ]);
         }
    -
    -    private function isViewable(Task $task): bool
    -    {
    -        if ($this->authorizationChecker->isGranted(User::ROLE_ADMIN)) {
    -            return true;
    -        }
    -
    -        $user = $this->currentUserProvider->getUser();
    -
    -        if ($this->authorizationChecker->isGranted(User::ROLE_MANAGER)) {
    -            return $user->getProjects()
    -                ->contains($task->getProject());
    -        }
    -
    -        return $task->getPerformedBy() === $user;
    -    }
     }

    Отлично! Повторения кода больше нет. Но что если мы усложним условия?
    Представим, что в списке у менеджера и разработчика должны выводиться только задачи, статус проекта которых не равен "archived".


    IsViewable.php
    use App\Entity\User;
     use App\Security\CurrentUserProvider;
     use App\Specification\AlwaysSpecified;
    +use App\Specification\AndX;
     use App\Specification\CompositeSpecification;
     use App\Specification\Equals;
     use App\Specification\Join;
     use App\Specification\MemberOf;
    +use App\Specification\Not;
    +use App\Specification\Project\IsArchived;
     use App\Specification\Specification;
     use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
    
    @@ -26,14 +29,23 @@ final class IsViewable extends CompositeSpecification
                 return new AlwaysSpecified();
             }
    
    +         $isNotArchived = new Not(new IsArchived()); 
             $user = $this->currentUserProvider->getUser();
    
             if ($this->authorizationChecker->isGranted(User::ROLE_MANAGER)) {
                 $isProjectMember = new MemberOf('members', $user);
    
    -            return new Join('task', 'project', $isProjectMember);
    +            return $this->getProjectSpecification(new AndX($isNotArchived, $isProjectMember));
             }
    
    -        return new Equals('performedBy', $user);
    +        return new AndX(
    +            new Equals('performedBy', $user),
    +            $this->getProjectSpecification($isNotArchived)
    +        );
    +    }
    +
    +    private function getProjectSpecification(Specification $specification): Join
    +    {
    +        return new Join('task', 'project', $specification);
         }
     }

    Выводы


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


    Да и вообще, что вы думаете о данном паттерне? Почему он так мало представлен в php? И можно ли ожидать, что он станет стандартом на уровне фреймворков?


    С полным примером из статьи можно ознакомиться на github.

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Как вы относитесь к паттерну «Спецификация»?

    • 22,2%Очень полезная вещь6
    • 22,2%От него больше вреда, чем пользы6
    • 55,6%Никогда о нем не слышал15

    Похожие публикации

    Средняя зарплата в IT

    113 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 10 036 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 14

      +3

      Вы строите свой query builder с equals, and, not поверх уже сделанного query builder. Зачем?

        0
        Композиция, переиспользование, возможность применять к отдельным объектам. querybuilder сам по себе ничего из этого не предоставляет.

        В статье есть ссылки на другие аспекты использования спецификаций, я же сосредоточился на переиспользовании бизнес-правил как для формирования выборки, так и для проверки отдельного объекта. Вот буквально пример из повседневной практики в дополнение к тем, что уже приведены в статье:
        При обновлении email пользователя подтвердить его изменение можно в течение 5 минут после отправки письма с кодом. При этом в одном месте приложения проверяется, что запрос еще актуален, а в другом формируется dql на очистку всех устаревших запросов. С помощью паттерна спецификация можно создать один класс IsActual и переиспользовать его как для проверки единичного запроса, так и для очистки всех устаревших.

        С помощью query builder вы напрямую это не сделаете. Разве что сможете проверить, что ваша единичная сущность входит в состав общей выборки. Но это как-то жестко что ли…
          0
          Композиция, переиспользование, возможность применять к отдельным объектам.

          Кажется все это query builder делает, я беру LINQ как пример. Композиция присутствует, переиспользование запросто — вытаскиваем то, что хотим реюзать в метод. Спецификации, должно быть, хороший способ реюзать запросы. Но заигрывания с композицией могут привести к необходимости обернуть все апи query object. А это уже код ради кода.

            0
            Можно ли применять LINQ к единичным объектам?

            В doctrine ORM query builder, по сути, является надстройкой над строками dql. Безусловно, куски $queryBuilder->andWhere('a = :b); можно выносить в отдельные методы, но насколько удобно будет их переиспользовать и как их композировать, нме не сильно понятно. Я применение такого подхода на практике вижу довольно редко.

            Вообще о паттерне «Спецификация» я впервые услышал применительно к C#. Там она построена на expression. Следовательно для него этот шаблон тоже актуален.

            Да и по поводу лишнего кода… Конечно, необходимо наладить свою инфраструктуру. Существующие библиотеки как-то пытаются это сделать. Но даже построение ее с нуля не выглядит сложной задачей. Дополнительные операции можно добавлять по мере необходимости и кода в них немного. Зато сами бизнес-правила довольно локаничны, выглядят понятно и предсказуемо.
              0

              В C# спецификация в виде отдельного объекта не особо нужна. Там можно написать что-то вроде db.Messages.Recipient(User).OrderBy(x => x.CreatedAt).Take(100); в результате получается Expression, к
              при желании эту строчку можно поместить в метод и реюзать где угодно. Этот Expression и есть встроенная в язык/рантайм спецификация. В любом другом языке, если есть достаточно мощный query builder, можно делать то же самое.

                0

                А эти выражения. размещенные по разным методам, можно объединять по and/or, использовать вложенно? Т.е. по примеру из статьи: создана спецификация для проекта IsArchived, но мы можем использовать ее при выборке задач, проект которых должен быть не archived.

            0
            создать один класс IsActual и переиспользовать его как для проверки единичного запроса, так и для очистки всех устаревших

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

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

              Ну, и раз вы пишете «теоретически», то в повседневной практике так не делаете? Как вы тогда решаете эту проблему? Просто пишите логику 2 раза?
                0

                Нет, создается класс UserQuery extends ActiveQuery, там пишутся методы типа isActive() { return $this->andWhere(['=', 'is_active', 1]); }, в коде делается так $users = User::find()->isActive().... Вот этот массив, которым задается условие, кладется в объект запроса, и теоретически их можно обрабатывать, чтобы проверить соответствие сущности.


                Я так не делал, обычно да, пишут проверки 2 раза. Но большинство из них простые, особых проблем это не создает, поэтому решение никто особо и не ищет.

                  0

                  Тогда ваш массив с условием и будет, по сути, спецификацией. Их кто-то и в строковом виде создает. Вопрос лишь в том, какие возможности предоставляет выразительность такого формата.


                  Реализация IsActual на основе статьи

                  sentAt для простоты хранится в timestamp


                  class ChangeEmailRequest
                  {
                      public int sentAt;
                      }
                  
                  final  class  IsActual extends CompositeSpecification
                  {
                      public function __construct(private int $lifeTime)
                      {
                      }
                  
                      public function getSpecification(): Specification
                      {
                          return new GreaterThan('sentAt', time() - $this->lifeTime);
                      }
                  }
                  
                  final  class  ChangeEmailController
                  {
                      public function confirm(ChangeEmailRequest $changeEmailRequest, IsActual $isActual): Response
                      {
                          if (!$isActual->isSatisfiedBy($changeEmailRequest)) {
                              throw new \Exception();
                          }
                  //...
                      }
                  }
                  
                  final  class  ChangeEmailRequestRepository
                  {
                      public function __construct(private IsActual $isActual)
                      {
                      }
                  
                      public function clearOldRequests(): void
                      {
                          $queryBuilder = $this->createQueryBuilder('r')
                              ->delete();
                          $isNotActual = new Not($this->isActual);
                          $isNotActual->filter($queryBuilder);
                          $queryBuilder->getQuery()
                              ->execute();
                      }
                  }
          0

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

            0

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


            Возможно, сам подход контринтуитивен. Плюс не для всех проектов описанные в статье проблемы так уж актуальны.


            Еще, вероятно, на более сложных примерах возникнет проблема натягивания спецификаций и на объект и на БД. Все-таки у ORM есть границы применимости.

              +1

              Ну вот в поддержке платформ я основную проблему и вижу.


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


              Сам подход вроде интуитивен, но многословен слишком


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

            0

            Все примеры, которые удавалось видеть — плохие и так или иначе приходилось возвращаться к простому Query Builder, а то и вообще чаще к SQL


            Сложные выборки строить через спецификацию = закраивать высокую связанность на уровне бизнес-сущностей

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое