If we are to explain the Specifications pattern, it would be; a logic that returns either true or false. The composite specification class has a standard method called isSatisfiedBy that returns the true or false values. This is done by checking given object to see if it satisfies the specifications set by us.


The scenario


You are selling your house and have some criterias that the user must meet in order to buy your house. The user must:



Example


User class


class User
{
private $age;
private $amountOwned;
private $isAlive;

public function __construct(int $age, float $amountOwned, bool $isAlive)
{
$this->age = $age;
$this->amountOwned = $amountOwned;
$this->isAlive = $isAlive;
}

public function getAge(): int
{
return $this->age;
}

public function getAmountOwned(): float
{
return $this->amountOwned;
}

public function getIsAlive(): bool
{
return $this->isAlive;
}
}

If we used a classic functional programming our code would look like below.


$saleAmount = 500000.00;

$user = new User(29, 700000.00, true);

if (
true === $user->getIsAlive() &&
$user->getAge() >= 18 &&
$user->getAmountOwned() >= $saleAmount
) {
echo 'Can buy'.PHP_EOL;
} else {
echo 'Cannot buy'.PHP_EOL;
}

// Result "Can buy"

Assume that we used this piece of code in multiple places of our application. If the rules change one day, we would potentially have some problems while reflecting the changes such as time wasting, making errors so on. Also it already doesn't look nice but if we had to add more checks to it, it would even look uglier.


In example below, we will implement both "hard coded specifications" and "parameterised specifications" in one go (you don't have to). The "hard coded specifications" doesn't need any argument being passed to the actual specification class - i.e. UserStillAliveSpecification and UserIsAdultSpecification. The "parameterised specifications" does need arguments being passed to the actual specification class - i.e. UserHasEnoughMoneySpecification.


interface SpecificationInterface
{
public function isSatisfiedBy(User $user): bool;
}

class UserStillAliveSpecification implements SpecificationInterface
{
public function isSatisfiedBy(User $user): bool
{
return $user->getIsAlive();
}
}

class UserIsAdultSpecification implements SpecificationInterface
{
private const MINIMUM_LEGAL_AGE_LIMIT = 18;

public function isSatisfiedBy(User $user): bool
{
return $user->getAge() >= self::MINIMUM_LEGAL_AGE_LIMIT;
}
}

class UserHasEnoughMoneySpecification implements SpecificationInterface
{
private $saleAmount;

public function __construct(float $saleAmount)
{
$this->saleAmount = $saleAmount;
}

public function isSatisfiedBy(User $user): bool
{
return $user->getAmountOwned() >= $this->saleAmount;
}
}

class House
{
private $specifications;

public function add(SpecificationInterface $specification)
{
$this->specifications[] = $specification;

return $this;
}

public function canBeSold(User $user): bool
{
/** @var SpecificationInterface $specification */
foreach ($this->specifications as $specification) {
if (!$specification->isSatisfiedBy($user)) {
return false;
}
}

return true;
}
}

$saleAmount = 500000.00;

$user = new User(29, 700000.00, true);

$house = (new House())
->add(new UserStillAliveSpecification())
->add(new UserIsAdultSpecification())
->add(new UserHasEnoughMoneySpecification($saleAmount));

$result = $house->canBeSold($user);

echo true === $result ? 'Can Buy' : 'Cannot buy';