DEV Community

clementDeb
clementDeb

Posted on

Le Pattern Builder et les données obligatoires

Introduction

Dans cet article je vous propose de redécouvrir le pattern builder permettant de rendre plus lisible la création d’objet. Nous verrons la principale problématique concernant l’utilisation des propriétés obligatoires et optionnelles dans notre objet et donc la possibilité de créer un objet incohérent par rapport au besoin métier. Nous verrons comment résoudre cette problématique pour s’assurer que notre builder nous renvoi un objet correctement construit.

L’utilisation d’un builder pour plus de lisibilité

Comme très bien expliqué par Colin DAMON dans son article les builders dans tous leurs états, le builder est intéressant pour la construction d'objets, en mettant à disposition une API dont les appels peuvent être chaînés et qui facilite la lecture et la définition de l’objet lorsque celui-ci dispose soit :
*de propriété de même type
*d’un nombre de propriétés important rendant compliqué l’instanciation par constructeur.

Dans cet article nous allons utiliser un objet de type Livre :

Image description

Pour définir cet objet, nous allons utiliser des données obligatoires: le titre et l’auteur, un Livre n’aurait pas de sens s’il était instancié sans, au minimum, ces deux propriétés.
Certaines propriétés étant de même type, String.class, en utilisant un constructeur classique, il est très facile de construire un objet incohérent :

Image description

Si on intervertit les propriétés titre et auteur, rien ne nous l’indique, de plus, n’ayant pas d’information sur l’ordre de ces propriétés dans le constructeur, l’erreur est vite arrivée. Une première solution serait d’éviter au maximum le même typage, ici un type Auteur pourrait être utilisé.
Nous analysons dans cet article le pattern Builder, donc voyons à quoi ressemble l’implémentation du builder de l’objet Livre :

Image description
Image description

  1. Nous définissons une inner class static.
  2. Les propriétés du builder sont les mêmes que les propriétés de notre objet
  3. Nous mettons à disposition des méthodes publiques ayant comme nom, le nom de la propriété que l’on souhaite initialiser. Le type de retour est important et correspond au builder lui-même, ce qui permet de chaîner les appels de manière fluide.
  4. Nous définissons la méthode finale du builder build() qui retourne un livre après l’avoir instancié en passant le builder en paramètre.
  5. Nous ajoutons le constructeur privé prenant le builder en paramètre dans la classe Livre.

Avec cette implémentation, nous empêchons l’instanciation d’un Livre de manière directe en obligeant le client à passer par le builder. L’utilisation du builder permet aussi une meilleure lisibilité des propriétés renseignées, exemple :

Image description

Problématique

Ce pattern est très courant, en revanche, une problématique non négligeable apparaît. Comment être sûr de créer un objet avec un état cohérent et donc avec les propriétés obligatoires correctement renseignées ?

Réponse : on ne peut pas. En tout cas pas au moment de la compilation, rien ne nous empêche d'appeler la méthode build() immédiatement après avoir instancié notre builder.
On peut rajouter une vérification au moment d'appeler build() sur les données obligatoires, mais cette vérification se fera au runtime et c’est dommage d’attendre que l’application tourne pour se rendre compte qu’on a potentiellement oublié quelque chose.
La validation au runtime n’est pas inutile, bien au contraire, mais si une erreur est levée au moment où on écrit le code, on ne va pas se plaindre.
Une solution serait de passer par le pattern “fluent Interface” et l’appliquer en plus du pattern Builder. Essayons et analysons.

Comment s’assurer de la cohérence métier de notre objet à la compilation.

Pour empêcher la création d’un objet incohérent, nous allons utiliser le pattern “Fluent Interface API pattern” pour implémenter notre builder. Ce pattern utilise des interfaces avec un type particulier qu’on utilise dans le retour de nos méthodes pour appeler la méthode de l’interface suivante.
Première étape : implémenter les interfaces correspondant à nos propriétés.

Image description

  1. On définit une interface pour chacune des propriétés obligatoires de notre objet, ayant pour unique méthode, la méthode de mise à jour de notre données.
  2. Le type de retour de notre méthode correspond à la donnée que l’on veut mettre à jour en suivant.
  3. Les méthodes de mise à jour des données non obligatoires sont positionnées dans la même interface, et renvoient le builder lui-même.
  4. La méthode finale build() est également positionnée dans cette interface et renvoie comme pour le builder classique, notre objet cible.

Deuxième étape : on implémente ces interfaces dans notre builder.

Image description

  1. On définit un constructeur privé pour bloquer l’instanciation directe et obliger le client à utiliser la static factory pour obtenir notre builder. Notre static factory renvoi le type de notre interface pour la donnée obligatoire concernant le titre.
  2. On implémente les méthodes de nos interfaces, et de la même manière que pour le builder classique, on utilise le builder lui-même comme type de retour.
  3. On implémente notre méthode finale build() qui, de la même manière que notre builder classique, est en charge de l’instanciation de notre objet cible.

Avec cette implémentation de notre builder, on est sûr que notre méthode finale build() ne sera appelée que lorsque notre objet Livre sera cohérent, donc avec ses deux données obligatoires renseignées.
En effet, l’utilisation de ce builder doit maintenant respecter un certain ordre dans son utilisation :

  • Pour appeler la méthode build(), il nous faut un objet de type LivreReste qui n’est renvoyé que par la méthode auteur(String auteur);
  • Pour appeler la méthode auteur(String auteur), il nous faut un objet de type LivreAuteur qui est renvoyé par la méthode titre (String titre)
  • Pour appeler la méthode titre (String titre), il nous faut un objet de type LivreTitre qui lui est renvoyé par la static factory get().

Exemple :

Image description

Tant que les deux données obligatoires ne sont pas renseignées, en respectant cet ordre, impossible d’appeler la méthode finale du builder.
Notre implémentation nous permettant de s’assurer que notre objet est correctement créé au moment de la compilation est finalisée.
Pour pouvoir récupérer un builder directement depuis la classe Livre, une static factory peut être implémentée :

Image description

Grâce à cette factory, l’appel du builder dans notre code se fait comme suit :

Image description

Vous allez me dire, que rien n'empêche que nos propriétés soient nulles. Vrai, d’où la nécessité d’ajouter une vérification au runtime de ces valeurs.

Ajout d’une validation des données au runtime

Rajoutons donc sur les propriétés obligatoires de notre builder les annotations javax.validation.NotEmpty ainsi que la méthode de validation de ces données dans la méthode build() :

Image description

  1. Les annotations @NotEmpty sont positionnées sur nos propriétés.
  2. La méthode de validation est appelée dans la méthode finale du builder.
  3. La méthode de vérification est implémentée en utilisant une ConstraintViolationException du package javax.validations, permettant de levée une exception avec un message du type:

    🚨javax.validation.ConstraintViolationException: titre: ne doit pas être vide

Avec cette implémentation de builder et de ses interfaces, ainsi que l’ajout de la validation des données au runtime, nous pouvons être plutôt serein sur la construction de notre objet et sur sa cohérence métier.

Conclusion

Dans cet article nous avons vu l’utilisation d’un builder nous permettant la construction d’un objet. Nous avons analysé et compris la problématique de l’utilisation de ce pattern. Une fois la problématique identifiée et comprise, une nouvelle implémentation du builder utilisant des interfaces est proposée dans le but d’ajouter une vérification au moment de la compilation de notre code.
Pour finir de sécuriser la cohérence métier de notre objet, nous avons également mis en place une validation au runtime avec une gestion d’exception.

code source utilisé dans l'artcicle

Top comments (0)