Be specific – We want to … – RulerZ



Be specific – We want to … – RulerZ

0 0


slides-apex-2015


On Github K-Phoen / slides-apex-2015

Be specific

Apex 2015

Kévin Gomez / @KPhoen

Application complexity keeps increasing

Depuis quelques années la complexité des applications web est à la hausse. Que ce soit côté frontend ou côté backend, nos applications et d'une manière générale les besoins auxquelles elles répondent sont de plus en plus complexes.

Design patterns / Languages / Tools / …

Pour répondre à cette tendance, les technologies que nous utilisons évoluent. On identifie des design patterns, on améliore nos langages de programmation et on développe de nombreux outils destinés à nous épauler dans nos tâches quotidiennes.

Agile / XP / BDD / DDD / …

Et en parallèle de nos outils, nos méthodes évoluent. Souvent avec une mise en avant du comportement de l'application, du métier ou du domaine.

« Web applications » over « Websites »

« Business rules » over « Code »

De l'évolution des besoins découle une évolution des priorités et des manières de développer des applications. On ne parle d'ailleurs plus de "site web" mais "d'application web". De même, notre attention se porte plus particulièrement vers les aspects métier de notre projet, vers le domaine que l'on modélise et ses règles de fonctionnement.

What should we do with these business rules?

On se rend compte qu'au delà des aspects purement techniques des applications web contemporaines se dégage une notion primordiale : celle du domaine métier et des règles qui le régissent. Ce sont finalement ces règles et cet univers qui sont complexes, passionnants et importants à modéliser. Mais ces règles, on en fait quoi dans nos applis ?

We want to …

  • … understand them ;
  • … easily express them ;
  • … compose them ;
  • … reuse them.
Ces règles, qui décrivent le cœur même du domaine que l'on modélise, doivent respecter quelques contraintes. D'un point de vue humain, elles doivent être facile à exprimer et à comprendre, de manière à restranscrire aussi fidèlement que possible le domaine. Puis d'un point de vue "code", elles doivent être facilement réutilisable, composable et d'une manière générale respecter toutes les bonnes pratiques de développement (respecter les principes SOLID, être testables, etc.).

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 L'idée principale de ce design pattern et de séparer la logique métier – les spécifications – des objets sur lesquels appliquer la logique – les « candidats ».

A specification = a business rule

Specifications are composable

Une spécification permet donc d'exprimer une et une seule règle métier. Cela nous force bien entendu à les isoler et à les exprimer clairement ! Pour exprimer des règles plus complexes, les spécifications sont composables.

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 »

Ici, on exprime comment déterminer si un livre peut être lu en ligne. On retrouve la règle métier ainsi que les méthodes permettant de la composer avec d'autres règles. C'est le squelette classique d'une spécification... si on omet les méthodes nécessaires à la composition de spécifications.

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() { /* … */ }
}
                
Avec ces méthodes on obtient une classe un peu moins jolie, avec du code boilerplate.

Usage

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

$isViewableOnline = $spec->isSatisfiedBy($book); // bool(true)
Mais d'un autre côté l'utilisation et la composition est sympa. Une spécification seule n'a que peu d'intérêt : la composition de plusieurs spécifications permet d'exprimer des règles métier plus complexes. On remarque qu'une spécification peut très bien être paramétrée.

Pros …

  • SOLID ;
  • unit-testable.
Au point où on en est, on essaie de prendre un peu de recul et d'analyser les avantages et inconvénients des spécifications quant à l'expression de nos règles métier et leur utilisation dans l'application. À priori, on respecte les principes SOLID et bonnes pratiques de développement. On peut même isoler une règle métier fondamentale et l'implémenter de manière testable.

… & cons

  • could be clearer ;
  • only usable on a Book instance.
Mais l'expression de la règle métier pourrait être plus claire et cette spécification n'est utilisable que sur un livre dont on a l'instance !

RulerZ

Features

  • data-agnostic DSL to express business rules ;
  • works at the instance level ;
  • works at the data-source level.
L'idée de RulerZ est simple : on garde nos spécifications mais on les exprime à l'aide d'un DSL (proche du SQL) et surtout on les utilise **aussi** pour récupérer les données satisfaisant ces spécifications. La même spécification peut donc à la fois servir de manière "classique" pour tester un objet donné mais aussi pour récupérer les objets la satisfaisant à partir d'une base de données, d'Elasticsearch, …

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 »

Le DSL est simple, extensible, volontairement très proche du SQL (mêmes objectifs !) et permet d'exprimer aisément nos règles métier.

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'],
        ];
    }
}
                
Bien entendu, travailler avec des objets représentant nos spécifications permet de les tester et composer plus facilement qu'avec de simples chaines de caractères. Une specification ne fait qu'encapsuler une règle textuelle en les rendant testables et en permettant de les réutiliser un peu partout dans la base de code.

Same usage

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

$isViewableOnline = $rulerz->satisfiesSpec($book, $spec); // bool(true)
Que ce soit avec une règle textuelle ou un object de spécification, vérifier qu'un objet lui est conforme est facile.

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>
Même règle ou spécification, mais cette fois on récupère des données au lieu de vérifier la validité d'un objet qu'on a déjà. Il est important de noter qu'à la place d'un QueryBuilder Doctrine, on aurait pu utiliser Pomm, ou un client Elasticsearch pour aller chercher les données ailleurs.

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() { }

    // …
}
                
Doctrine les met en avant mais c'est une pratique répandue : les Repository permettent d'isoler l'accès au données du reste de l'application. Le soucis vient de l'explosion du nombre de méthodes à implémenter pour permettre d'accéder aux données ... pas très SOLID !

Repositories – After

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

        return $this->rulerz->satisfiesSpec($qb, $spec);
    }
}
                
Ce problème est résolu par l'utilisation conjointe des spécifications et de RulerZ, dans une méthode matching

Other use cases

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

L'exemple classique des bons de réduction : un administrateur peut saisir lui-même via une interface les conditions d'utilisation d'un bon de réduction.

Hoa & RulerZ

Architecture

Pour parler de la place de Hoa dans RulerZ, je dois rapidement présenter son architecture. Grossièrement, pour qu'une règle puisse être "utilisée" elle doit être : - parsée : une représentation intermédiaire résulte de cette opération ; - puis compilée vers la cible d'exécution voulue : en utilisant cette représentation intermédiaire. RulerZ tire 95% de la grammaire de son DSL, 95% de son Parseur et sa représentation intermédiaire de Hoa\Ruler. Hoa\Ruler tient donc un rôle primordial dans RulerZ !

Dependencies

Hard: Hoa\Ruler

Implied: Hoa\Compiler

Thanks!

Going further
Be specific Apex 2015 Kévin Gomez / @KPhoen