Press [s] to read the speaker notes (will open a new window).
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.Languages / Tools / Design patterns / …
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 / 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. En tant que développeur, nous produisons de la valeur ajoutée en écrivant du code. Ou plus exactement en développant des outils destinés à répondre aux besoins de nos clients/utilisateurs. Mais au delà du code, un développeur produit le plus de valeur ajoutée quand il arrive, le plus fidèlement possible, à retranscrire dans l'application qu'il conçoit le coeur des règles métier de son client.« A book supports the web reader if it's an ePub not protected by Adobe DRM »
Pour imager un peu plus mes propos, voyons concrètement comment implémenter une spécification. Je travaille pour TEA - The Ebook Alternative, une entreprise qui édite une plateforme de vente et distribution de ebooks. Parmi les fonctionnalités de la plateforme on retrouve un webreader qui permet de lire certains ebooks directement dans le navigateur. Nous allons tenter d'implémenter la règle qui indique si un livre donné peut-être ou non lu par ce webreader. À savoir : « un livre peut être lu dans le webreader si c'est un ePub non protégé par Adobe DRM ».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() { } // … }Par des repositories "à la Doctrine" ? Pas vraiment. On encapsule bien la couche de stockage sous-jacente, mais on a peu de pouvoir expressif, il est difficile de combiner des critères et chaque nouvelle "requête" nécessite l'écriture d'une méthode dans ce repository.
A specification = a business rule
Specifications are composable
Une spécification permet donc d'exprimer une et une seule règle métier. Chaque règle étant encapsulée dans une classe qui la représente. Pour exprimer des règles plus complexes, les spécifications sont composables (ET, OU et NON).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 »
Le pattern Specification nous dit que notre règle métier sera représentée par une classe. Chacune des spécifications que nous écrirons exposera une méthode "isSatisfiedBy" qui permettra de déterminer si un objet donné - ici un livre - satisfait la spécification. L'implémentation de cette méthode relève directement de la traduction de notre règle métier en code. Seulement, rien ne nous permet de composer cette spécification avec d'autres…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 »
Pour ceci on a besoin de trois nouvelles méthodes qui seront les mêmes pour chacune des spécifications que nous écrirons.$spec = (new SupportsWebReader()) ->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 agréable à écrire et à relire. 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. N.B : la spécification ainsi obtenue représente une "vraie" règle utilisable dans la plateforme. On peut imaginer qu'elle permet de tester si un livre peut être lu en ligne en france.
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. On retrouve ici la règle écrite précédemment.$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)L'utilisation est aussi simple qu'habituellement. On a d'un côté notre règle textuelle, de l'autre l'objet à tester et on passe les deux à RulerZ. On obtient toujours en sortie un booléen.
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 »
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.// build a specification object $spec = (new SupportsWebReader()) ->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 toujours aussi facile.
// 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>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. Une règle métier exprimée une seule fois dans le code permet donc de valider un objet ou de retrouver tous les objets la validant. Plus de duplication ! En bonus, l'utilisations est aussi simple que pour un objet unique : la règle et la source de données sont tout ce dont RulerZ à besoin.
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 !
class DoctrineBookRepository implements BookRepository { public function matching(Specification $spec) { $qb = $this->createQueryBuilder('book'); 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. Une méthode sachant retourner les données correspondant à une spécification, des spécifications décrivant ces données, le tour est joué ! N.B : il faut bien entendu rester pragmatiques. Rien n'empêche d'avoir cette méthode "matching" en conjonction d'autres méthodes plus "techniques" dont la valeur ajoutée est faible en terme de métier (récupérer un objet par son identifiant par exemple). L'utilisation de spécification doit se faire en priorité lorsque la valeur métier le justifie.
One field → One specification object
Face à un formulaire aussi massif que celui-ci et destiné uniquement à filtrer des données on peut considérer chaque champ comme une "spécification". En tirant parti (par exemple) du composant Form de Symfony on peut aisément convertir chaque valeur saisie par l'utilisateur en son équivalent en spécification et se reposer sur RulerZ pour tout ce qui concerne le filtrage des données.
e-commerce coupons / …
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. Ces règles d'attributions permettront à la fois de vérifier qu'un bon de réduction peut être utilisé par un client, mais aussi d'être proactif et d'envoyer un mail aux clients concernés pour les prévenir et les pousser à l'achat.