Be specific

DDD Day 2016

Kévin Gomez / @KPhoen

Press [s] to read the speaker notes (will open a new window).

Application complexity keeps increasing

Languages / Tools / Design patterns / …

Agile / 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 ;
  • … reuse them ;
  • … compose them.

But… how do we do that?

Webreader

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

Repositories "à la Doctrine" ?


class DoctrineBookRepository implements BookRepository
{
    public function add(Ebook $book) { }
    public function remove(Ebook $book) { }

    public function findByEan($ean) { }
    public function findByTitle($title) { }
    public function findPublished() { }
    public function findViewableOnline() { }
    public function findNotViewableOnline() { }

    public function findPublishedAndViewableOnline() { }

    // …
}
                

What does the Bible say?

“A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction.”
Martin Fowler — http://martinfowler.com/eaaCatalog/repository.html

What does the Bible say?

Specifications

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 SupportsWebReader 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 SupportsWebReader 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() { /* … */ }
}
                

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

Usage


$spec = (new SupportsWebReader())
    ->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"';

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

Same specification


class SupportsWebReader extends AbstractSpecification
{
    public function getRule()
    {
        return 'format IN :formats_epub AND protection != "adobe drm"';
    }

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

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

Same usage


// build a specification object
$spec = (new SupportsWebReader())
    ->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 the database
$queryBuilder = $entityManager
    ->createQueryBuilder()
    ->select('book')
    ->from('Entity\Book', 'book');

// and we want to find the viewable online books
$viewableOnlineBooks = $rulerz->filterSpec($queryBuilder, $spec);

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

Under the hood

Architecture

RuleParserCompilerPHP codeSQL queryES queryIRTargets

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('book');

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

Search forms


One field → One specification object

Wallabag

Tagging rules


Other use cases

e-commerce coupons / …

Going further

Thanks!

Kévin Gomez / @KPhoen