Apprendre + Équipe = Programmes


(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 :

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 :

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 :

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 :

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 :

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;