Article

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!

Edwin Kortman

Software Developer