Purpose
The purpose of the design pattern
Builds a clear specification of business rules, where objects can be checked against. The composite specification class has one method called isSatisfiedBy that returns either true or false depending on whether the given object satisfies the specification.
A great use case for this is to validate objects. Example (with unit test) below.
Examples
Examples of how the design pattern can be used
UML
UML design pattern diagram
Code
Code snippets
Item
Basic Item class which accepts a float ($price) via the construct. A getter method getPrice returns the price.
namespace DesignPatterns\Behavioral\Specification;
class Item
{
public function __construct(private float $price)
{
}
public function getPrice(): float
{
return $this->price;
}
}
Specification
Specification Interface. A contract that sub classes must adhere to if they implement it. This interface forces classes which implement it to have a isSatisfiedBy method.
namespace DesignPatterns\Behavioral\Specification;
interface Specification
{
public function isSatisfiedBy(Item $item): bool;
}
AndSpecification
AndSpecification is a concrete class which implements Specification. It accepts multiple Specifications via construct, then it’s isSatisifiedBy method loops through each one and checks each classes version of isSatisifiedBy. If one of them returns false, isSatisified for AndSpecification fails.
namespace DesignPatterns\Behavioral\Specification;
class AndSpecification implements Specification
{
/**
* @var Specification[]
*/
private array $specifications;
/**
* @param Specification[] $specifications
*/
public function __construct(Specification ...$specifications)
{
$this->specifications = $specifications;
}
/**
* if at least one specification is false, return false, else return true.
*/
public function isSatisfiedBy(Item $item): bool
{
foreach ($this->specifications as $specification) {
if (!$specification->isSatisfiedBy($item)) {
return false;
}
}
return true;
}
}
NotSpecification
NotSpecification is a concrete class which implements Specification. It accepts a Specification via construct, then it’s isSatisifiedBy methods check the inverse of the Specification passed to it. I.e if the specifications isSatisifedBy returns false, then we return true (NotSpecification works out to the true).
namespace DesignPatterns\Behavioral\Specification;
class NotSpecification implements Specification
{
public function __construct(private Specification $specification)
{
}
public function isSatisfiedBy(Item $item): bool
{
return !$this->specification->isSatisfiedBy($item);
}
}
OrSpecification
OrSpecification is a concrete class which implements Specification. It accepts multiple Specifications via construct, then it’s isSatisifiedBy method loops through each one and checks each classes version of isSatisifiedBy. If one of them returns true, isSatisified for OrSpecification succeeds, otherwise it fails.
namespace DesignPatterns\Behavioral\Specification;
class OrSpecification implements Specification
{
/**
* @var Specification[]
*/
private array $specifications;
/**
* @param Specification[] $specifications
*/
public function __construct(Specification ...$specifications)
{
$this->specifications = $specifications;
}
/*
* if at least one specification is true, return true, else return false
*/
public function isSatisfiedBy(Item $item): bool
{
foreach ($this->specifications as $specification) {
if ($specification->isSatisfiedBy($item)) {
return true;
}
}
return false;
}
}
PriceSpecification
PriceSpecification is a concrete class which implements Specification. It accepts a minPrice and maxPrice via construct, then it’s isSatisifiedBy method checks if an Item’s given price is within minPrice and maxPrice, if it is, then isSatisfiedBy returns true, otherwise false.
namespace DesignPatterns\Behavioral\Specification;
class PriceSpecification implements Specification
{
public function __construct(private ?float $minPrice, private ?float $maxPrice)
{
}
public function isSatisfiedBy(Item $item): bool
{
if ($this->maxPrice !== null && $item->getPrice() > $this->maxPrice) {
return false;
}
if ($this->minPrice !== null && $item->getPrice() < $this->minPrice) {
return false;
}
return true;
}
}
SpecificationTest
SpecificationTest is a unit test which tests each aspect of the work above.
namespace DesignPatterns\Behavioral\Specification\Tests;
use DesignPatterns\Behavioral\Specification\Item;
use DesignPatterns\Behavioral\Specification\NotSpecification;
use DesignPatterns\Behavioral\Specification\OrSpecification;
use DesignPatterns\Behavioral\Specification\AndSpecification;
use DesignPatterns\Behavioral\Specification\PriceSpecification;
use PHPUnit\Framework\TestCase;
class SpecificationTest extends TestCase
{
public function testCanOr()
{
$spec1 = new PriceSpecification(50, 99);
$spec2 = new PriceSpecification(101, 200);
$orSpec = new OrSpecification($spec1, $spec2);
$this->assertFalse($orSpec->isSatisfiedBy(new Item(100)));
$this->assertTrue($orSpec->isSatisfiedBy(new Item(51)));
$this->assertTrue($orSpec->isSatisfiedBy(new Item(150)));
}
public function testCanAnd()
{
$spec1 = new PriceSpecification(50, 100);
$spec2 = new PriceSpecification(80, 200);
$andSpec = new AndSpecification($spec1, $spec2);
$this->assertFalse($andSpec->isSatisfiedBy(new Item(150)));
$this->assertFalse($andSpec->isSatisfiedBy(new Item(1)));
$this->assertFalse($andSpec->isSatisfiedBy(new Item(51)));
$this->assertTrue($andSpec->isSatisfiedBy(new Item(100)));
}
public function testCanNot()
{
$spec1 = new PriceSpecification(50, 100);
$notSpec = new NotSpecification($spec1);
$this->assertTrue($notSpec->isSatisfiedBy(new Item(150)));
$this->assertFalse($notSpec->isSatisfiedBy(new Item(50)));
}
}