Шаблон спецификации реализации
Попытка использовать шаблон спецификации и столкнулась с проблемой его работы в разных реализациях (например, в памяти, 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 или что-либо еще?
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 в репозиторий.
- Спецификация определяет, какой запрос следует использовать.
- Фильтры возвращают набор (частично или полностью) из репозитория.