(Abstraction + Encapsulation) ❤️ (Aligner Tech + Métier)
Dans une discussion récente, un collègue s'interrogeait sur sa vision du code, et se demandait quel était le "bon curseur" sur l'abstraction à utiliser par des développeurs·euses pour à la fois exprimer le métier du produit, et conserver une maîtrise sur la complexité du code.
J'ai trouvé sa question intéressante, et j'ai noté ci-dessous ce qu'elle m'a évoqué.
Tout d'abord, je n'ai pas de réponse absolue à ce que serait le bon curseur. J'aurais envie de conseiller de commencer par chercher des références sur les concepts d'abstraction et d'encapsulation. Ensuite de reconnaître dans nos pratique ce qui y ressemble, et de pratiquer en équipe ce qu'on a appris ensemble.
Définitions
Mais pour préciser un peu, qu'est-ce que j'appelle abstraction et encapsulation ? Ces concepts ont été popularisés par la programmation orientée objet, et pour moi ils en sont le cœur. Cependant ces concepts sont indépendants de ce paradigme, et je peux les utiliser dans mes réflexions, quel que soit le paradigme utilisé.
Voilà comment Grady Booch (et al.) les définissait dans son livre Object-Oriented Analysis and Design :
Abstraction focuses on the essential characteristics of some object, relative to the perspective of the viewer.
Encapsulation hides the details of the implementation of an object.
Ce sont des notions proches et qui vont ensemble. L'abstraction permet de prendre le point de vue de l'observateur·rice et de se concentrer sur les caractéristiques essentielles. Elle permet parfois aussi de généraliser un comportement, comme les classiques Abstract Data Types. Si je prends l'exemple d'une pile : quels que soient les objets qu'elle contient, une pile permet d'empiler (push) et de dépiler (pop). Push et pop sont les caractéristiques essentielles qui la définissent.
L'encapsulation cache les détails d'implémentation, c'est à dire qu'en tant qu'utilisateur d'une pile, on ne doit pas savoir comment elle est implémentée. Ça peut être un tableau, ou une liste chaîné, ce détail ne doit pas changer son usage où que ce soit dans le code. À part dans le code de son implémentation.
Perspective
J'ai utilisé l'exemple de la pile pour illustrer, mais ces concepts vont plus loin, et en tirent d'autres. Ils rejoignent souvent l'architecture et le design de code. Voilà trois exemples de plus haut niveau, dont on n'a à mon avis pas encore tiré tous les bénéfices :
- La notion de couches, un peu comme le modèle OSI mais au niveau code.
- La notion d'injection de dépendances, qui permet de rendre le code testable et modifiable quand on a des dépendances volatiles.
- Plus finement, la question qu'on peut se poser ensemble dès qu'on regarde un bout de code : "est-ce que ces 5 lignes de code manipulent des éléments qui sont au même niveau d'abstraction ?"
En passant, ces notions sont parfois utilisées par la communauté DDD, mais elles ont de la valeur en soi, même si on ne fait pas de DDD.
La notion de couche permet d'organiser le code par niveaux d'abstraction. Elle nous invite à réfléchir ensemble comment on veut structurer et concrétiser nos couches. Les notions de module et d'API (au sens large de Programming Interface, d'un module ou autre) peuvent nous aider à les rendre concrètes.
Par exemple on peut imaginer d'avoir une couche métier, dans laquelle on ne va avoir que des notions métier. Quand on lit et écrit du code dans cette couche, on ne réfléchit qu'au métier, sans mélanger les problèmes de formats de fichiers, et sans avoir à se souvenir comment fonctionne notre ORM ni comment écrire un Group By. Les considérations techniques ne nous encombrent pas. Cet exemple est le pattern Domain Model. Je paraphrase Romeu Moura : même si on était capable de réfléchir à tout en même temps, pourquoi est-ce qu'on s'infligerait ça ?
Note : comme tout pattern, le pattern Domain Model a ses contextes où il est utile, et ses contextes où il apporte plus de complexité que d'utilité. C'est la même chose pour la notion de couches, à vous de voir si une organisation en couches est pertinente dans votre cas.
La notion d'injection de dépendances permet de rendre explicites les relations entre couches par exemple, voire même de les inverser (avec un petit formalisme supplémentaire). Si je fais un plat de spaghettis où tout dépend de tout, mes injections de dépendances vont me le montrer. En plus de ça, injecter les dépendances permet de faciliter l'écriture de tests automatiques.
Et la question "est-ce que ces 5 lignes de code manipulent des éléments qui sont au même niveau d'abstraction" peut souvent mener à démêler les plats de spaghettis justement, voire à éviter de les créer.
Quand je dis qu'on n'a pas encore tiré tous les bénéfices de ces trois points, je veux dire que dans les projets que j'observe et auxquels je participe, ces trois points sont à améliorer en permanence. Ils font l'objet d'un équilibre instable, et nous demandent un effort pour les maintenir. Il y a des idées et des outils qui aident, comme :
- La revue de code
- Les tests automatisés
- Le refactoring
- L'architecture hexagonale
Mais en conserver la cohérence demande de la maintenance au quotidien, et de la communication dans l'équipe.
Abstraction et encapsulation : pas automatique
Je disais plus haut que j'essaie d'introduire de la complexité quand son coût est inférieur aux bénéfices qu'on en retire : j'aime aussi rester pragmatique.
Pour donner un exemple caricatural je peux écrire un script shell que je n'utiliserai qu'une fois sans aucune abstraction, avec des for et des if imbriqués, tant que je peux vérifier facilement et tout au long de l'écriture qu'il fonctionne comme je l'imagine.
J'ajoute ici nouvelle idée, dont à mon avis on n'a pas encore tiré tous les bénéfices. Quand j'écris du code, même quand je n'utilise ni abstraction, ni encapsulation, ni aucun pattern précis, je me demande toujours comment est-ce que je vais pouvoir :
- Vérifier qu'il marche au fil de l'écriture et pas à la fin ?
- Le modifier quelques temps plus tard en m'assurant que je n'ai rien cassé ?
Dans le cas précédent, le script shell à usage unique, ça peut être une série de printf qui vont valider mes hypothèses au fil de l'eau. Ou pourquoi pas, des tests automatisés quand les vérifications commencent à être nombreuses. Je remarque aussi que très vite, ces vérifications vont m'inciter à introduire au moins de l'encapsulation, pour pouvoir faire des vérifications indépendantes des implémentations.
Maintenance et Alignement Métier
Et pour revenir au titre, je me dis aussi que ces activités de maintenance et d'équilibre, nos choix d'abstraction et l'organisation de notre code, ont d'autant plus de sens si on les mène dans la perspective d'aligner la technique et le métier. Il ne s'agit surtout pas de faire une abstraction ou une encapsulation techniquement parfaite, mais qui rendrait l'expression du besoin métier plus complexe que nécessaire. Sinon on n'en retirera pas les bénéfice, voire on le paiera. Sandy Metz en donne des exemples dans ses réflexions et conférences sur la "Wrong Abstraction", par exemple :
Prefer duplication over the wrong abstraction.
Et que si on développe à plusieurs (i.e. dans mon cas, tout le temps), c'est important que ces activités soient l'occasion de discussions qui créent de l'alignement entre nous, sur la tech et le métier, sinon on passe également à côté du sujet.
Là encore, la communauté DDD s'intéresse beaucoup à aligner le code avec le métier. Et dans son livre Domain-Driven Design, Eric Evans utilise beaucoup le mot "abstraction", pour en effet prendre le point de vue du métier, et en exprimer l'essentiel (il parle même de distiller à un moment). Pour ne citer que les deux premières occurrences du mot :
A domain model [...] is not just the knowledge in a domain expert’s head; it is a rigorously organized and selective abstraction of that knowledge.
Et :
The abstractions are true business principles.
Conclusion
J'ai été content de réfléchir à ces sujets, et de regrouper :
- Une définition de l'abstraction et de l'encapsulation.
- Des patterns de design de plus haut niveau.
- Un rappel rapide de se poser la question si ces patterns sont pertinents dans votre contexte.
- Les questions de vérification que le code fonctionne.
- La notion de maintenance.
- L'alignement de la tech et du métier.
Et je me rends compte que j'ai évoqué là, en accéléré, une grande partie de ce à quoi je réfléchis à chaque fois que je travaille avec du code. Et vous, à quoi est-ce que vous réfléchissez quand vous travaillez avec du code ?
Bonus
La notion de frontière
Quand je vois que de l'encapsulation va être bénéfique, j'utilise beaucoup la notion de frontière. Par ordre croissant :
- Un bloc de scope est une frontière
- Une fonction est une frontière
- Une interface est une frontière
- Un module est une frontière
- Une couche est une frontière
- Une API est une frontière
Deux exemples d'encapsulation hors programmation objet
Si vous avez lu jusque là, et que vous voulez en savoir plus, voilà deux exemple d'abstraction et d'encapsulation hors du paradigme de programmation objet.
Cet article montre comment faire des types de données abstraits et même de l'encapsulation en programmation fonctionnelle (ici Haskell) :
Et en programmation structurée, en C par exemple : on peut masquer les détails d'implémentation en ne mettant que les déclarations dans les fichiers .h, et en cantonnant les implémentations dans les fichiers .c. On peut même cacher les détails de la représentation des données en ne déclarant que des pointeurs vers des structures dans les fichier .h, par exemple :
typedef struct dictionnary *Dictionnary;