Blog Arolla

Plus de classes abstraites grâce au TDD !

Discours entre deux singes. Le premier dit : Tien toi qui n'aimes pas les classes abstraites, je viens de voir cet article qui les met en avant avec TDD, et la guenon après avoir lu l'article lui répond : tu as lu l'article, ca dit le contraire.

Encore un titre confusant ! En vrai, le titre aurait dû être "Je n'ai plus de classes abstraites grâce au TDD !".

Depuis quelques années, j'ai constaté que je n'écris plus de classes abstraites dans mon code et je me suis donc posé la question "Pourquoi je n'ai plus de classes abstraites dans mon code ?". En creusant la question, je me rends compte que je n'utilise plus de classe abstraite depuis que j'ai commencé à appliquer le TDD ("Test-Driven Development" ou développement dirigé par les tests). J'ai donc décidé d'écrire cet article pour montrer qu'on peut oublier les classes abstraites au profit de la composition.

Mais je vais d'abord commencer par les définitions de ce qu'est le TDD et qu'est-ce une classe abstraite.

Qu’est-ce que le TDD ?

Kent Beck a créé la méthode du développement dirigé par les tests dans les années 2000. « Test-driven Développement » est une méthode qui prône une nouvelle gymnastique de l’esprit et une nouvelle discipline : écrire le test avant le code de production. L’idée est donc de se fixer un objectif à atteindre en écrivant un test. Ce dernier permet de savoir quand s’arrêter de développer le comportement attendu. Dans un premier temps, le test sera rouge à l’exécution puisque le comportement du code correspondant n'existe pas encore. Attention la phase rouge ne veut pas dire que le code ne compile pas. On doit donc avoir créé manuellement ou généré via l’IDE la méthode à appeler. La deuxième phase correspond à écrire le minimum de code pour que le test passe au vert. Et enfin la factorisation du code permet de l'améliorer et d’obtenir une meilleure lisibilité et maintenabilité en troisième phase. On parle alors du cycle "Red Green Refactor" comme présenté dans l'image ci-dessous.
Cycle Ref/Green/Refactor de TDD
Le développement dirigé par les tests nécessite tout de même quelques prérequis. En effet, avant d'attaquer l'écriture du premier test, il vaut mieux définir les "baby steps". Ce sont de petites étapes pour arriver à la solution finale attendue.

Classe abstraite

C'est une classe dont l'implémentation n'est pas complète (https://fr.wikipedia.org/wiki/Classe_abstraite). Elle peut être héritée mais ne peut pas être instanciée. On peut y définir une méthode abstraite dont le corps est implémenté dans une classe fille. Elle peut contenir des méthodes ou des attributs implémentés (privés ou publics).
Souvent on l'utilise pour factoriser du code.

Prenons l'exemple suivant :

Le montant total est égal à la somme du montant hors taxe et la tva. Par contre la TVA varie selon le pays. Pour la France, elle est de 19,6%. Par contre imaginons que pour Le Luxembourg, le calcul est tout autre. Il y a une taxe de 12% sur le service et une taxe de 15% sur le matériel.
Quand on factorise pour créer une classe mère abstraite, on peut définir le calcul du montant total qui est commun aux deux classes filles. Par contre ce qui est spécifique aux classes filles, on le définit en tant qu'abstrait au niveau de la classe mère. Ainsi on a juste à le spécifier au niveau des classes filles. Ci-dessous une solution de factorisation de notre exemple. Elle implémente le « design pattern » (modèle de conception) « Template method ». Ce pattern fait partie de ceux du Golf (Gang of Four) décrit dans le livre « Design Patterns: Éléments of Reusable Object-Oriented Software » écrit par Erich Gamma, Richard Helm, Ralph Johnson, et John Vlissides, avec une préface de Grady Booch.
Classe abstraite pour refactorer le comportement commun des classes filles
Vu comme ça, la classe abstraite est bien utile pour rendre le code plus lisible et pour bien séparer ce qui est commun de ce qui est spécifique.
Il y a quelques années, c'était la solution que je préconisais surtout après une conception faite comme elle se doit, avant de commencer les développements 😏.En tout cas, c'est ce que j'ai appris à l'école et pendant mes premières expériences en tant que développeuse junior. Et puis, ça paraît propre, lisible et bien factorisé !
Mais en utilisant une classe abstraite, on ne peut jamais tester unitairement la fonctionnalité commune vu qu'on ne peut pas l'instancier, il faut toujours passer par une classe fille pour le faire.
On va donc voir le même test se répéter autant de fois que de classes filles si on veut être sûr d'avoir bien respecté le principe de Liskov (L de SOLID : Une classe mère peut être substituée par une classe fille sans que le comportement change). Et puis le comportement de la classe mère est testé autant de fois qu’on a de tests sur les comportements des classes filles.
Quand on n'a que deux classes filles, la tâche est simple mais imaginons le cas d'une dizaine de classes filles, c'est un peu galère, non !

Comment implémenter sans classe abstraite ?

Lors de mes expériences, j'ai rencontré plusieurs façons de développer. Ce qui m'a permis de comparer, d'analyser et de comprendre comment mieux implémenter, même si je suis certaine qu’il y a potentiellement d’autres méthodes et que la mienne est encore perfectible.
Et puis un jour j'ai découvert le principe "Composition over inheritance" dans le livre du GoF cité dans le paragraphe précédent.
Ce principe invite à faire de la composition des objets pour implémenter la fonctionnalité voulue.
J'ai commencé à l'appliquer dans mes développements. Au début, c'était difficile de s'affranchir de l'héritage quand j'avais l'habitude de l'appliquer et surtout j'étais convaincue de l'utilité de la factorisation avec une classe abstraite.
Avec le temps j'ai appris la méthode TDD et je me suis rendue compte que j'utilisais de moins en moins l'héritage.

Reprenons l'exemple du calcul de la taxe. Quand je le fais en TDD et en baby step, je vais décomposer mon problème en trois parties. Le calcul nominal du montant total, le calcul de la TVA française et celui du Luxembourg.
Pour calculer le montant total, on a besoin de la valeur de la tva et le montant hors taxe qui vont être récupérées. Et pour chacun des pays, on a le calcul des valeurs de la tva.

Ainsi, on va avoir la classe OrderPrice qui contient la méthode CalculateAmount().
On va donc créer les tests pour cette méthode. Sauf que pour faire les calculs, on a besoin du montant hors taxe et du montant de la tva. Pour cela, on crée une interface qui retourne ces deux résultats. A ce stade, on a besoin de la classe à tester (OrderPrice) et l'interface IVatCalculator (sans implémentation) comme le décrit le schéma suivant. Ainsi, on peut tester unitairement la fonctionnalité de calcul en substituant l'interface injectée.
OrderPrice avec la méthode CalculateAmount qui utilise IVatCalculator avec le calcul de TVA et la pre-taxe

En deuxième temps, on va préparer les tests correspondant à l'une des implémentations de l'interface IVatCalculator, suivis par ceux de la deuxième implémentation. En résultat, le diagramme de classes ressemble à ce qui suit.
Deux implémentations de IVatCalculator : French et Luxembourgish

Avant de finir

La démarche que je viens de décrire correspond à l'approche "outside in" en TDD (appelée aussi approche mockiste). Le développement est guidé par le besoin et l'implémentation de la solution se fait de l'extérieur vers l'intérieur en utilisant des mocks.
Robert Martin et Sandro Mancuso ont fait toute une série de vidéos pour expliquer la différence entre les deux approches (outside in Vs Inside out) par ici

Conclusion

J'espère que j'ai apporté suffisamment d'informations pour vous montrer qu'on peut se passer des classes abstraites en utilisant la composition. Le code reste tout aussi clair voir plus clair qu'avec l'héritage.
N'hésitez pas à revenir vers moi pour en discuter sur les réseaux sociaux (LinkedIn). Je serai ravie de débattre sur le sujet !

A bientôt ! 🙂

Plus de publications

Comments are closed.