Be specific

Apex 2015

Kévin Gomez / @KPhoen

Application complexity keeps increasing

Design patterns / Languages / Tools / …

Agile / XP / BDD / DDD / …

« Web applications » over « Websites »

« Business rules » over « Code »

What should we do with these business rules?

We want to …

  • … understand them ;
  • … easily express them ;
  • … compose them ;
  • … reuse them.

The specification pattern

“The central idea of Specification is to separate the statement of how to match a candidate, from the candidate object that it is matched against.”
Martin Fowler — http://martinfowler.com/apsupp/spec.pdf

A specification = a business rule

Specifications are composable

Sample specification


class BookSupportsWebReader implements Specification
{
    const FORMATS_EPUB = ['epub', 'epub 3', 'epub fixed layout'];

    public function isSatisfiedBy($book)
    {
        return in_array($book->getFormat(), self::FORMATS_EPUB)
            && $book->getProtection() !== 'adobe drm';
    }
}
                

« A book supports the web reader if it's an ePub not protected by Adobe DRM »

Sample specification


class BookSupportsWebReader implements Specification
{
    const FORMATS_EPUB = ['epub', 'epub 3', 'epub fixed layout'];

    public function isSatisfiedBy($book)
    {
        return in_array($book->getFormat(), self::FORMATS_EPUB)
            && $book->getProtection() !== 'adobe drm';
    }

    public function andX(Specification $spec)
    {
        return new AndSpecification($this, $spec);
    }
    public function orSatisfies(Specification $spec) { /* … */ }
    public function not() { /* … */ }
}
                

Usage


$spec = (new BookSupportsWebReader())
    ->andX(new AvailableInCountry('FR'))
    ->andX((new PublisherBlacklisted())->not());

$isViewableOnline = $spec->isSatisfiedBy($book); // bool(true)

Pros …

  • SOLID ;
  • unit-testable.

… & cons

  • could be clearer ;
  • only usable on a Book instance.

RulerZ

Features

  • data-agnostic DSL to express business rules ;
  • works at the instance level ;
  • works at the data-source level.

Same rule


format IN :formats_epub AND protection != "adobe drm"

« A book supports the web reader if it's an ePub not protected by Adobe DRM »

Same usage


$rule = 'format IN :formats_epub AND protection != "adobe drm" AND
"fr" IN countries AND NOT(publisher.blacklisted)';

// use the textual rule
$isViewableOnline = $rulerz->satisfies($book, $rule, [
    'formats_epub' => ['epub', 'epub 3', 'epub fixed layout'],
]); // bool(true)

Same specification


class BookSupportsWebReader implements Specification
{
    public function getRule()
    {
        return 'format IN :formats_epub AND protection != "adobe drm"';
    }

    public function getParameters()
    {
        return [
            'formats_epub' => ['epub', 'epub 3', 'epub fixed layout'],
        ];
    }
}
                

Same usage


// build a specification object
$spec = (new BookSupportsWebReader())
    ->andX(new AvailableInCountry('FR'))
    ->andX((new PublisherBlacklisted())->not());

$isViewableOnline = $rulerz->satisfiesSpec($book, $spec); // bool(true)

Usage on a datasource


// our app uses Doctrine to query our database
$queryBuilder = $entityManager
    ->createQueryBuilder()
    ->select('book')
    ->from('Entity\Book', 'book');

// textual rule
$viewableOnlineBooks = $rulerz->filter($queryBuilder, $rule, [
    'formats_epub' => ['epub', 'epub 3', 'epub fixed layout'],
]);

// specification object
$viewableOnlineBooks = $rulerz->filterSpec($queryBuilder, $spec);

var_dump($viewableOnlineBooks); // array<Entity\Book>

A few use cases

Repositories – Before


class DoctrineBookRepository implements BookRepository
{
    public function findByEan($ean) { }
    public function findByTitle($title) { }
    public function findPublished() { }
    public function findViewableOnline() { }
    public function findNotViewableOnline() { }

    public function findPublishedAndViewableOnline() { }

    // …
}
                

Repositories – After


class DoctrineBookRepository implements BookRepository
{
    public function matching(Specification $spec)
    {
        $qb = $this->createQueryBuilder('c');

        return $this->rulerz->satisfiesSpec($qb, $spec);
    }
}
                

Other use cases

Dynamic rules (e-commerce coupons) / Search forms / …

Hoa & RulerZ

Architecture

Dependencies

Hard: Hoa\Ruler

Implied: Hoa\Compiler

Thanks!

Going further