Шаблон спецификации реализации


Попытка использовать шаблон спецификации и столкнулась с проблемой его работы в разных реализациях (например, в памяти, orm и т. Д.). Мой основной ORM - это доктрина, что означает, что моим первым выбором было использовать критерии использования спецификаций, поскольку они работают с коллекциями массивов (для реализаций памяти) и на ORM. К сожалению, они довольно ограничены в типах запросов, которые они могут выполнять (не могут выполнить соединение).

В качестве примера предположим, что у меня есть Спецификация UserHasBoughtProduct, которой в конструкторе присваивается идентификатор продукта. Спецификация очень проста для написания на наивном уровне.

public function isSpecifiedBy(User $user)
{
    foreach ($user->getProducts() as $product)
    {
        if ($product->getId() == $this->productId)
        {
            return true;
        }
    }

    return false;
}

Однако, что, если бы я хотел найти всех пользователей, которые купили продукт? Мне нужно было бы передать эту спецификацию в мой пользовательский каталог с помощью какого-то метода findSpecifiedBy (Спецификация $спецификация);. Но это не работает в рабочей среде, так как пришлось бы проверять каждого отдельного пользователя в базе данных.

Моя следующая идея отсюда заключалось в том, что спецификация была только интерфейсом, а реализация осуществлялась инфраструктурой. Итак, в моем каталоге persistence\doctrine\user\ у меня может быть спецификация продукта userhasbought, а в моем каталоге persistence\InMemory\user у меня есть другая. В некотором смысле это работает, но очень раздражает необходимость использования в коде, так как мне нужно, чтобы все мои спецификации были доступны либо в контейнере DI, либо на какой-либо фабрике. Не говоря уже о том, что если у меня есть класс, который требует несколько спецификаций, которые мне понадобятся, чтобы ввести их все через конструктор. Плохо пахнет.

Было бы гораздо предпочтительнее, если бы я мог просто сделать следующее в методе:

$spec = new UserHasBoughtProductSpecification($productId);
$users = $this->userRepository->findSatisfiedBy($spec);
//or
if ($spec->isSatisfiedby($user))
{
//do something
}

У кого-нибудь был опыт выполнения этого в PHP? Как вам удалось реализовать шаблон спецификации таким образом, чтобы он работал в реальном мире и мог использоваться в различных бэкэндах, таких как InMemory, ORM, чистый SQL или что-либо еще?

Author: Anti-Dentite, 2015-10-25

1 answers

Если вы объявляете Спецификацию в качестве интерфейса в своем Домене и внедряете ее в инфраструктуру, вы перемещаете бизнес-правила в инфраструктуру. Это противоположно тому, что делает DDD.

Таким образом, бизнес-правила Specification должны быть размещены на уровне домена.

Когда Specification используется для проверки объектов , работает очень хорошо. Проблема возникает, когда используется для выбора объекта из коллекции, в данном случае из Repository, из-за большого количества объекты в памяти могут быть.

Чтобы избежать встраивания бизнес-правил в Repository и утечки деталей SQL в Domain, Эрик Эванс в своей книге DDD дает нам несколько решений:

1. Двойная отправка + специализированный запрос

    public class UserRepository()
    {
        public function findOfProductIdBought($productId)
        {
            // SQL
            $result = $this->execute($select);

            return $this->buildUsersFromResult($result);
        }    

        public function selectSatisfying(UserHasBoughtProductSpecification $specification)
        {
            return $specification->satisfyingElementsFrom($this);
        }
    }


    public class UserHasBoughtProductSpecification()
    {
        // construct...

        public function isSatisfyBy(User $user)
        {
            // business rules here...
        }

        public function satisfyingElementsFrom($repository)
        {
            return $repository->findOfProductId($this->productId);
        }
    }

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

2. Двойная отправка + общий запрос

Другое решение - использовать более общий запрос.

public class UserRepository()
{
    public function findWithPurchases()
    {
        // SQL
        $result = $this->execute($select);

        return $this->buildUsersFromResult($result);
    }    

    public function selectSatisfying(UserHasBoughtProductSpecification $specification)
    {
        return $specification->satisfyingElementsFrom($this);
    }
}


public class UserHasBoughtProductSpecification()
{
    // construct ...

    public function isSatisfyBy(User $user)
    {
        // business rules here...
    }

    public function satisfyingElementsFrom($repository)
    {
        $users = $repository->findWithPurchases($this->productId);

        return array_filter($users, function(User $user) {
            return $this->isSatisfyBy($user);
        });
    }
}

Оба решения:

  • Храните бизнес-правила в одном месте, в Домене.
  • Помещает SQL в репозиторий.
  • Спецификация определяет, какой запрос следует использовать.
  • Фильтры возвращают набор (частично или полностью) из репозитория.
 9
Author: martinezdelariva, 2015-10-26 09:29:41