Attempting to use the specification pattern and have run into the issue of having it work in different implementations (e.g., in memory, orm, etc.). My main ORM is Doctrine, which means that my first choice was to have the specifications use Criterias as they work on ArrayCollections (for InMemory implementations) and on the ORM. Unfortunately they are fairly limited in the sorts of queries they can run (can't perform a join).
As an example let's say I have a UserHasBoughtProduct specification that is given a product id in the constructor. The specification is very simple to write at a naive level.
public function isSpecifiedBy(User $user)
{
foreach ($user->getProducts() as $product)
{
if ($product->getId() == $this->productId)
{
return true;
}
}
return false;
}
However, what if I wanted to find all users that had bought the product? I would need to pass this specification to my UserRepository via some sort of findSpecifiedBy(Specification $specification); method. But this doesn't work in production as it would have to check every single user in the database.
My next idea from here was that the specification be only an interface and the implementation was handled by the infrastructure. So, in my persistence\doctrine\user\ directory, I might have a UserHasBoughtProduct specification and in my persistence\InMemory\user directory I have another. This works, in a way, but is very annoying to have to use in code as I will need all of my specifications to be available either by a DI container or in some sort of factory. Not to mention that if I have a class which requires several specifications I would need to inject them all via the constructor. Smells bad.
It would be much more preferable if I could simply do the following in a method:
$spec = new UserHasBoughtProductSpecification($productId);
$users = $this->userRepository->findSatisfiedBy($spec);
//or
if ($spec->isSatisfiedby($user))
{
//do something
}
Has anyone had experience doing this in PHP? How did you manage to implement specification pattern in such a way that it works in the real world and is usable in different backends such as InMemory, ORM, pure SQL, or anything else?
If you declare Specification as an interface in your Domain and implement it in the infrastructure, you are moving business rules to the infrastructure. That's the opposite of what DDD does.
So, the Specification
business rules has to be place in Domain layer.
When Specification
is used to validate objects, works very well. The issue comes when is used to select an object from collection, in this case, from the Repository
, due to the large number of objects in memory may be.
In order to avoid embedding business rules into the Repository
and leaked SQL details into the Domain
, Eric Evans in his book of DDD, give us several solutions:
1. Double dispatch + specialized query
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
has a specialized query, that matches exactly with our Specification
.
Although this kind of query can be acceptable E. Evans points us this most likely will be used only in this case.
2. Double dispatch + generic query
Another solution is use more generic query.
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);
});
}
}
Both solutions:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With