Specifying specific rules with a pattern
July 2nd, 2018
The Specification Pattern is a nifty set of classes which are great for chaining tricky business rules using boolean logic. It is a pattern frequently used in the context of domain-driven design and we had an interesting use-case for it.
Let's start off by painting a picture. I want you to think about implementing happy little memberships. You are an application developer and you want to coincide the vast amount of rules in code.
Assuming we are the core developer for "Public Library And Co", a company focused solely on renting books. All the hassle with rental periods, book conditions, dynamic prices etcetera aside you as a member can take a pick from at least twenty memberships. For example if you want to get a youth membership these rules apply:
- Signee is between the ages eighteen and twelve.
- Attending highschool.
- If there is an active membership for this person, it should without debt.
Combine this with the x variants of a membership and you will soon get a 'wirwar' of if else
statements. This is a valid use-case for the specification pattern.
Given a set of classes and one interface AndSpecification
, OrSpecification
, NotSpecification
, CompositeSpecification
and the interface Specification
.
In the example CompositeSpecification
has been left out because in PHP and
, or
and alike are keywords that, up un till PHP 7, were not available as method names. You could refactor this to use the keywords but that's personal preference. I prefer a tree-like structure for business rules.
<?php
final class AndSpecification implements Specification
{
/**
* @var Specification[]
*/
private $specifications;
public function __construct(Specification ...$specifications)
{
$this->specifications = $specifications;
}
public function isSatisfiedBy($candidate): bool
{
foreach ($this->specifications as $specification) {
if (!$specification->isSatisfiedBy($candidate)) {
return false;
}
}
return true;
}
}
<?php
final class OrSpecification implements Specification
{
/**
* @var Specification[]
*/
private $specifications;
public function __construct(Specification ...$specifications)
{
$this->specifications = $specifications;
}
public function isSatisfiedBy($candidate): bool
{
$isSatisfied = false;
foreach ($this->specifications as $specification) {
if ($specification->isSatisfiedBy($candidate)) {
$isSatisfied = true;
}
}
return $isSatisfied;
}
}
<?php
final class NotSpecification implements Specification
{
/**
* @var Specification
*/
private $specification;
public function __construct(Specification $specification) {
$this->specification = $specification;
}
public function isSatisfiedBy($candidate): bool
{
return !$this->specification->isSatisfiedBy($candidate);
}
}
<?php
interface Specification
{
public function isSatisfiedBy($candidate): bool;
}
Implementing the rules
Reflecting what we are trying to achieve here. We want to have a human-readable format for a set of business rules expressed and applied in code. The rules for applying or switching to a youth memberships are: - Signee is between the ages eighteen and twelve. - Attending highschool. - If there is an active membership for this person, it should without debt.
We can extract four unique specifications; IsBetweenAgesEighteenAndTwelve
, IsAttendingHighschool
, DoesHaveAnActiveMembership
and MembershipDoesNotHaveDebt
.
These four class names are all classes that implement the Specification
interface with logic to test the specification added in isSatisfiedBy
. The $candidate
variable could be filled with the Membership to be tested.
Combining this will result in a specification for validating a new youth membership.
<?php
// ...
$youthMembershipSpecification = new AndSpecification(
new IsBetweenAgesEighteenAndTwelve(),
new IsAttendingHighschool(),
new OrSpecification(
new AndSpecification(
new DoesHaveAnActiveMembership(),
new MembershipDoesNotHaveDebt()
),
new NotSpecification(
new DoesHaveAnActiveMembership()
)
)
);
$isAbleToSignup = $youthMembershipSpecification->isSatisfiedBy($membership);
// ... Error handling and stuff!
Software Developer
Do you have similar issues? Contact us at Future500
We can tackle your technical issues while you run your business.
Check out what we do or write us at info@future500.nl