Builder Pattern : Une Version Typescript Compatible

11 min read / December 30, 2021

Ça fait maintenant un bon bout de temps que j'utilise NestJs pour des projets persos et pros et je dois dire que je suis assez conquis. Notamment son système d'injection de dépendance chipé à Angular qui simplifie l'architecture de l'application et la mise en place des tests.

Le framework étant très orienté autour de la POO et du DDD, il y a cependant une partie que je n'arrivais pas à craquer : la création rapide et pratique des Domain Models.

La meilleure solution que j'avais trouvée était une adaptation du Simple Factory Pattern qui même si elle fonctionnait bien, me laissant cet arrière goût de Mouais...

Note au passage

Le code est en libre accès sur github et disponible sur npm sous le nom builder-pattern-2 :

Plantons le décors

Pour cet article, partons du principe qu'on développe une entreprise de Serviteurs On Demand. Nos clients sont des personnes qui ont un problème précis comme l'incapacité à ouvrir un pot de cornichons. Ils se rendent sur notre plateforme, commandent un serviteur qui leur vient en aide et disparait instantanément une fois la tâche accomplie. Appelons ces serviteurs des Mr Meeseeks.

Mr Meeseeks, toujours prêt à se payer une bonne tranche en aidant son prochain.

Voilà le Domain Model de notre Mr Meeseeks :

Il a un objectif à atteindre (goal) et une durée de vie maximale (lifespan) avant d'avoir le droit au repos.

Le Simple Factory Pattern à l'Arrière Goût de Mouais

Afin de créer un Mr Meeseeks, jusqu'à maintenant j'aurais utilisé une adaptation du Simple Factory Pattern. Plutôt que de passer tous les paramètres du constructeur à la main, je passe par un object de configuration :

L'object de config est 1. extensible, 2. je n'ai pas à passer undefined aux paramètres optionnels qui ne m'intéressent pas et 3. je peux définir des valeurs par défaut pour les paramètres obligatoires. Par exemple, si les analytics de la boite nous permettent de nous apercevoir que l'ouverture de pots de cornichons est un véritable customer pain point, on peut potentiellement le mettre en goal par défaut dans la factory (ça ne ferait pas nécessairement sens de le mettre en valeur par défaut dans le constructeur de la classe).

Là où ça pêche c'est que je dois mettre à jour manuellement l'interface CreateArgs et l'implémentation de la méthode create à chaque fois que j'ajoute un paramètre au constructeur. Si j'oublie l'une des 2 étapes, ça ne fonctionne pas, ce qui alourdi considérablement la charge mentale liée à la modification d'un Domain Model. Pareil pour la création d'un Domain Model et même davantage, puisque je dois implémenter la factory qui n'existe pas encore.

Mon postulat c'est qu'il doit y avoir moyen de générer la factory automatiquement à partir d'un Domain Model plutôt que de le faire à la main.

Le Builder Pattern à l'Élégance Fringante

Comme j'étais insatisfait, j'ai commencé à regarder les solutions existantes à gauche à droite et je me suis souvenu du décorateur @Builder du package Java Lombok.

Dans un monde plein de magie où javascript exécuterait du code java car ils ont leurs 4 premières lettres communes, le décorateur @Builder de Lombok donnerait le résultat suivant :

Le Builder Pattern est élégant, pratique et lisible notamment grâce à sa Fluent API qui permet de chaîner les méthodes du builder. De plus, il se plug simplement sur la classe qu'on veut rendre buildable sans autres formes d'intrusivité.

De cette utilisation simple découlent des contraintes non triviales que le système doit respecter :

  1. Génération automatique des méthodes du builder en fonction des paramètres du constructeur : le builder doit exposer la méthode setLifespan comme le constructeur prend le paramètre lifespan.
  2. Auto-complétion des méthodes du builder avec typescript.
  3. Inférence des types des méthodes du builder avec typescript : la méthode setLifespan doit prendre un number en paramètre et retourner un Builder de MrMeeseeks.
  4. Pas de code boilerplate à ajouter dans la classe à rendre buildable : le système doit être le moins invasif possible pour se laisser la possibilité de changer l'implémentation du builder si on le souhaite.

On va s'occuper de l'implémentation en 2 parties : dans un premier temps, on va s'occuper de générer le builder de façon automatique (javascript) et dans un second temps on implémentera le typage (typescript).

Génération du Builder

Le builder sera une classe qui a pour attributs les paramètres du constructeur de la classe qu'il construit. Par exemple, pour créer un MrMeeseeks, on a doit passer 2 paramètres qui sont goal et lifespan. Le builder d'un MrMeeseeks aura donc 2 attributs qui seront goal et lifespan, dont la valeur sera définie par les méthodes setGoal et setLifespan.

Commençons simple et implémentons la fonction qui crée un builder à partir d'une classe. Le builder est une classe instantiable, donc la base de la fonction est :

La première étape est de définir les attributs du builder. Pour ça il nous faut récupérer le nom des paramètres du constructeur de cls.

En javascript, il est possible de récupérer le code d'une classe en la convertissant en string. C'est à la fois weird et très pratique puisqu'avec une regex on va pouvoir extraire la partie constructeur de la classe et ses paramètres :

En appliquant extractConstructorParams à la classe MrMeeseeks, on obtient le tableau de string ['goal', 'lifespan'].

On peut maintenant créer les attributs et setters du builder :

Avec ça on obtient déjà un builder javascript fonctionnel.

Un exemple d'utilisation :

Ici on n'a pas spécifié le lifespan de notre MrMeeseeks avec le builder qui pourtant vaut Infinity. Puisque l'attribut lifespan du builder vaut undefined par défaut, l'attribut de la classe prend la valeur par défaut définie par le constructeur.

Typage du Builder

L'objectif est d'obtenir l'auto-complétion des méthodes du builder avec leur typage. Par exemple, si je fais appel au builder, je veux que la méthode setGoal me soit proposée et que je saches qu'elle prend en paramètre une string et retourne un Builder de MrMeeseeks.

Typescript propose un ensemble d'Utility Types qui facilitent la manipulation des types.

Un qui potentiellement nous intéresse est ConstructorParameters. Il extrait le type des paramètres du constructeur d'une classe :

C'est intéressant, on vient de récupérer les types des paramètres du constructeur mais ça s'arrête là parce que typescript ne permet pas de récupérer le nom des paramètres pour le moment.

C'est ici que commence le compromis vers notre builder idéal.

Puisque les noms des setters reposent sur le noms des paramètres du constructeur, une solution est de les répertorier dans une interface. Les clés seront les noms des paramètres et les valeurs leur type :

Et depuis typescript 4.1, la génération des noms des setters devient facile grâce au Template Literal Types. Ils permettent de créer des types string de manière programmatique à partir d'autres types string. Par exemple, le type string "goal" peut être utilisé pour créer le type string "setGoal" sans qu'on ait besoin de le créer à la main.

La manipulation des types string en typescript fonctionne de la même manière qu'en javascript :

Le plus gros du travail est fait. Il reste à définir le type Builder qui doit être constructible par l'opérateur new et exposer une méthode build :

Tout ça mis bout à bout, la séquence suivante est valide du point de vue du typage :

Dernier Coup de Polish

Dernière étape, il nous reste à typer la fonction qui génère le builder :

Nous voilà maintenant avec un builder 100% fonctionnel.

Épilogue

Un dernier snippet de code pour la route histoire d'illustrer son utilisation :

GameBoy

© 2023 - Baboo