5. — WHAT IS A SPECIFICATION? —
Design pattern
used to codify business rules
that state something about an object
6. Cheap to
WRITE
MAINTAIN
Complex
LOGIC
Easy to
TEST
To encapsulate a business rule which does not belong inside entities or value, but is applied to it.
— WHY TO USE A SPECIFICATION? —
To make a business rule explicit in the codebase.
To make a business rule a first-class citizen of your codebase - domain.
8. — ASSERTION —
interface CustomerSpecification
{
public function isSatisfiedBy(Customer $customer) : bool;
}
final class CustomerIsPremium implements CustomerSpecification
{
private $orderRepository;
public function __construct(OrderRepositoryInterface $orderRepository)
{
$this->orderRepository = $orderRepository;
}
public function isSatisfiedBy(Customer $customer) : bool
{
return $this->orderRepository->countFor($customer) > 3;
}
}
$customer = $customerRepository->findById(42);
$spec = new CustomerIsPremium($orderRepository);
$spec->isSatisfiedBy($customer);
9. — ASSERTION - COMPOSITE - AND —
final class AndSpecification extends AbstractSpecification implements SpecificationInterface
{
/** @var AbstractSpecification */
private $one;
/** @var AbstractSpecification */
private $two;
/**
* @param AbstractSpecification $one
* @param AbstractSpecification $two
*/
public function __construct(AbstractSpecification $one, AbstractSpecification $two)
{
$this->one = $one;
$this->two = $two;
}
/**
* @param mixed $object
*/
public function isSatisfiedBy($object) : bool
{
return $this->one->isSatisfiedBy($object) && $this->two->isSatisfiedBy($object);
}
}
10. — ASSERTION - COMPOSITE - OR —
final class OrSpecification extends AbstractSpecification implements SpecificationInterface
{
/** @var AbstractSpecification */
private $one;
/** @var AbstractSpecification */
private $two;
/**
* @param AbstractSpecification $one
* @param AbstractSpecification $two
*/
public function __construct(AbstractSpecification $one, AbstractSpecification $two)
{
$this->one = $one;
$this->two = $two;
}
/**
* @param mixed $object
*/
public function isSatisfiedBy($object) : bool
{
return $this->one->isSatisfiedBy($object) || $this->two->isSatisfiedBy($object);
}
}
11. — ASSERTION - COMPOSITE - NOT —
final class NotSpecification extends AbstractSpecification implements SpecificationInterface
{
/** @var AbstractSpecification */
private $spec;
/**
* @param AbstractSpecification $spec
*/
public function __construct(AbstractSpecification $spec)
{
$this->spec = $spec;
}
/**
* @param mixed $object
*/
public function isSatisfiedBy($object) : bool
{
return !$this->spec->isSatisfiedBy($object);
}
}
12. — ASSERTION - COMPOSITE —
$specAnd = new AndSpecification(
new CustomerIsPremium($orderRepository),
new CustomerHasOverdueInvoices($invoiceRepository)
);
$specAnd->isSatisfiedBy($customer);
$specOr = new OrSpecification(
new CustomerIsPremium($orderRepository),
new CustomerHasOverdueInvoices($invoiceRepository)
);
$specOr->isSatisfiedBy($customer);
$CustomerIsNotPremium = new NotSpecification(
new CustomerIsPremium($orderRepository)
);
$specNot->isSatisfiedBy($customer);
13. — ASSERTION - COMPOSITE - PIPE IT BABY —
abstract class AbstractSpecification
{
/**
* @param AbstractSpecification $other
* @return SpecificationInterface
*/
public function andSpecification(AbstractSpecification $other)
{
return new AndSpecification($this, $other);
}
/**
* @param AbstractSpecification $other
* @return SpecificationInterface
*/
public function orSpecification(AbstractSpecification $other)
{
return new OrSpecification($this, $other);
}
/**
* @return SpecificationInterface
*/
public function notSpecification()
{
return new NotSpecification($this);
}
}
15. — SELECTION —
interface SqlSpecification
{
/** @return string */
public function asSql();
}
final class CustomerIsPremium implements CustomerSpecification, SqlSpecification
{
// ...
/** @return string */
public function asSql()
{
return "SELECT * FROM customers LEFT JOIN orders ON ...";
}
}
16. — SELECTION —
interface RepositoryCompatibleSpecification
{
public function applyToRepository(SpecificationReceivableRepository $repo);
}
interface RepositoryCompatibleSpecification
{
public function getRecordsMatching(RepositoryCompatibleSpecification $spec) : array;
}
17. — SELECTION —
EXAMPLE OF SPECIFICATION USED FOR SELECTION WITH DOCTRINE
https://github.com/Happyr/Doctrine-Specification
23. — BUILD TO ORDER —
final class Basket
{
private $products = array();
private $amount = 0.0;
public function __construct(array $products = array())
{
foreach ($products as $product) {
$this->products[$product->getProductId()] = $product;
$this->amount += $product->getPrice();
}
}
public function hasProduct(Product $product)
{
return isset($this->products[$product->getProductId()]);
}
public function getAmount()
{
return $this->amount;
}
public function getProducts()
{
return $this->products;
}
}
24. — BUILD TO ORDER —
class BasketOperationService
{
public function addProductToBasket(Basket $basket, Product $product)
{
$products = $basket->getProducts();
$products[$product->getProductId()] = $product;
$newBasket = new Basket($products);
return $newBasket;
}
public function removeProductFromBasket(Basket $basket, Product $product)
{
$products = $basket->getProducts();
unset($products[$product->getProductId()]);
$newBasket = new Basket($products);
return $newBasket;
}
}
25. — BUILD TO ORDER —
class ProductIsInBasketSpecification
{
public function isSatisfiedBy(Basket $basket, Product $product)
{
return $basket->hasProduct($product);
}
}
26. — BUILD TO ORDER —
class BasketCanBeBoughtSpecification
{
public function isSatisfiedBy(Basket $basket, User $user)
{
if ($basket->getAmount() <= $user->getCurrentBalanceAmount()) {
return true;
}
return false;
}
}
27. — BUILD TO ORDER —
final class TriedToAddProductToBasketEvent
{
private $basket;
private $product;
private $user;
public function __construct(Basket $basket, Product $product, User $user)
{
$this->basket = $basket;
$this->product = $product;
$this->user = $user;
}
public function getProduct()
{
return $this->product;
}
public function getBasket()
{
return $this->basket;
}
public function getUser()
{
return $this->user;
}
}
29. — BUILD TO ORDER —
class TriedToAddProductToBasketEventSubscriber
{
public function onTriedToAddProductToBasketEvent(TriedToAddProductToBasketEvent $event, EventDispatcher $dispatcher)
{
$basket = $event->getBasket();
$product = $event->getProduct();
$user = $event->getUser();
$communicatorService = $dispatcher->getFrontendCommunicatorService();
$productIsInBasketSpecification = new ProductIsInBasketSpecification();
if ($productIsInBasketSpecification->isSatisfiedBy($basket, $product)) {
$communicatorService->showError('Product already in basket!', $product);
} else {
$basketOperationService = new BasketOperationService();
$newBasket = $basketOperationService->addProductToBasket($basket, $product);
$basketCanBeBoughtSpecification = new BasketCanBeBoughtSpecification();
if ($basketCanBeBoughtSpecification->isSatisfiedBy($newBasket, $user)) {
/* ... Replace $basket with $newBasket in DIC for further events to use it ... */
$communicatorService->showProductInBasket($product);
$communicatorService->setTotalPrice($newBasket->getAmount());
} else {
$communicatorService->showError('Not enough funds!', $product);
}
}
}
}
32. — FURTHER READING —
“THE BLUE BOOK”
https://www.amazon.com/gp/product/B00794TAUG/
EXAMPLE OF SPECIFICATION USED FOR SELECTION WITH DOCTRINE
https://github.com/Happyr/Doctrine-Specification
BLOG POST 4 QUICK PEEK
http://marcaube.ca/2015/05/specifications
MARTIN FOWLER
https://www.martinfowler.com/apsupp/spec.pdf
“THE YELLOW BOOK”
https://leanpub.com/ddd-in-php?a=Ug88MJbcykCAu8AEAWNjDA
33. Q&A
Robert Šorn
COO @ TRIKODER
robert.sorn@trikoder.net
ks, ks, BTW, Trikoder is hiring! ;)
Head of PM, PHP developers, Java developers, UX/UI specialist, …
work@trikoder.net