Programmation par intention (Intentional Programming)

Après avoir assisté à l’excellente présentation “Crafting code” de Sandro Mancuso lors de NCrafts 2015, j’ai eu envie de partager avec vous un aspect fondamental du métier de développeur : la programmation par intention. Le premier chapitre du livre “Essential skills for the agile developer” (que je vous recommande d’ailleurs) en est dédié, ainsi que l’excellent livre de “Clean Code” de Robert C. Martin. La programmation par intention (intentional programming en anglais) est une approche de programmation qui vous propose de mettre en exergue dans votre code l’intention de vos actions à développer. J’entends ici par ‘actions’ les cas d’utilisation, les actions métiers concrètes que doit faire votre programme (code). Par exemple : ajouter un utilisateur, payer une commande, exécuter une transaction, calculer le prix total, etc… Ces actions pourraient se traduire par des noms de classe, de namespaces, de méthodes affichant clairement l’intention du programmeur : des méthodes CalculerPrixTotal et PayerCommande, une classe TransactionBancaire, etc
Le constat actuel
Cela peut paraître évident pour certains développeurs expérimentés ou bien formés, et pourtant beaucoup d’applications informatiques ne présentent pas toujours la caractéristique d’être “codées par intention”. Cela pose problème, car il devient difficile de corriger un bug, de faire évoluer ou de modifier un fonctionnalité existante dans le programme : tout simplement parce que le développeur qui lit un code dont il ne comprend pas l’intention aura du mal à l’appréhender. Il prendra au mieux plus de temps, et au pire introduira lui-même des dysfonctionnements.
Un grand nombre de développeurs pour de multiples raisons plus ou moins discutables ont tendance à complexifier leur code (sans s’en rendre compte sur le moment d’ailleurs), même en ayant une bonne intention au départ :
- Ils utilisent parfois certains “designs patterns” pour des raisons obscures (pour le plaisir d’en utiliser même si cela n’est pas nécessaire) : on se retrouve avec des factories à tout va et parfois tout un empilement de designs patterns entremêlés qui, au final, rendent le code difficilement lisible pour tout nouveau développeur intervenant sur ce code.
- Ils rendent génériques du code en vue d’une éventuelle réutilisation ultérieure (YAGNI !)
- Ils ne prennent pas le temps de nommer correctement les variables, classes et fonctions développées, en générale parce que leur hiérarchie et/ou le business (time to market) ne leur permettent pas de prendre le temps de “bien faire”, ou qu’ils ne s’obligent pas eux-même à avoir cette rigueur dans leur façon de coder (tout un débat !).
- Ils développent des fonctions qui effectuent des actions multiples comprises dans un processus entier d’exécution.
- Il ne pensent jamais à refactorer, ne pratiquent pas de code review.
- Ils ne testent pas unitairement leur code (sujet à débat également), et au passage pas d’intégration continue…
- Bref, tout un ensemble de mauvaises pratiques qui s’installent au fil du temps.
Bien entendu, cela n’est pas le cas partout (ce ne sont que des exemples parmi d’autres), et nous tombons tous dans ce genre de travers, mais il est important d’apprendre de nos erreurs et de toujours tendre vers une amélioration continue. Notre code se doit d’être de qualité et pour cela, commençons par écrire du code simple en expriment clairement nos intentions dans nos méthodes/classes/modules.
Intérêt général de la programmation par intention
Matérialiser son intention dans le code passe par un ensemble de bonnes pratiques qu’on se doit de s’approprier en tant que développeur. Cependant, on peut commencer par de bonnes bases : le nommage de nos classes/méthodes/namespaces doit exprimer clairement l’intention de leurs actions. La programmation par intention préconise des méthodes ayant une responsabilité unique (le S de SOLID). Tout comme dans la vie courante, il est plus aisé et plus simple de faire une seule chose à la fois. En respectant le paradigme de la programmation par intention, tout un ensemble d’avantages se feront sentir tout le long du développement technique d’un projet informatique :
Facilité de lecture : commentaires inutiles
Plus nous programmons par intention, moins nous aurons besoin de commentaires pour notre code, car les méthodes, variables, classes, etc seront nommées par intention : la lecture du code entraînera implicitement sa compréhension. Aussi, des méthodes et classes courtes avec des responsabilités limitées et bien définies nous donneront une bonne compréhension de l’intention de tout ou partie du code lu. Les commentaires seront réduits à expliquer des algorithmes trop complexes pour être compris de la seule lecture du code.
Debugging
Quand on programme par intention, on a tendance à écrire des méthodes qui ont la responsabilité d’une action unique : de ce fait, il devient plus évident de détecter d’où viendrait un éventuel bug dans notre code lors de “debuggage”. A l’inverse, des méthodes trop longues ayant plusieurs responsabilités vont demander plus d’efforts, ne serait-ce que pour la compréhension de son intention par le développeur qui l’analyse.
La cohésion des méthodes
La cohésion exprime le degré de relation d’une méthode/classe/module par rapport aux autres méthodes/classes/namespaces avec lesquelles elle est en relation. Globalement, plus on limitera le couplage (relations fortes d’une classe avec une autre par exemple), et plus on limitera à une responsabilité unique (SRP), mieux le code sera de qualité (plus lisible, plus simple, et donc plus maintenable, testable et évolutif).
Exemple de code
Comparez les bouts de code suivants :
// calcul des frais engendrés par une commande public int MontantFrais(int x, int y, int z) { var montant = 0; // Montant des frais est égale à la somme des frais de base montant = x + z; // si le montant des frais annexes est supérieure à 5, //on fait payer le client if (z > 5) { montant = montant + z; } return montant; }
à celui-ci :
public int CalculFraisTotal(int fraisTransport, int fraisforfaitaire, int fraisAnnexes) { var fraisFixes = CalculFraisFixes(fraisTransport, fraisforfaitaire); var fraisAnnexes = CalculFraisAnnexes(fraisAnnexes); return fraisFixes + fraisAnnexes; } private int CalculFraisFixes(int fraisTransport, int fraisforfaitaire) { var fraisFixes = fraisTransport + fraisIndirect; return fraisFixes; } private int CalculFraisAnnexes(int fraisAnnexes) { if (fraisAnnexes > 5) { return fraisAnnexes; } return 0; }
Lequel à votre avis est plus lisible ?
Refactoring & facilité d’évolutivité
Lorsque nous refactorons du code, les questions que nous nous posons souvent sont :
- A quoi peut bien correspondre ce code ?
- Qu’a t-il voulu faire exactement dans cette méthode ?
- C’est bizarre, les commentaires dans cette fonction ne correspondent pas au code écrit…
- J’ai l’impression que la méthode “calculTauxFixe” fait exactement le même traitement que la fonction “calculTauxGeneral” et la fonction “calculTaux” qui sont dans des classes différentes.
- J’ai demandé au gars qui a codé la fonction (une chance qu’il soit encore là….) mais visiblement, il n’est plus très sûr de ce que fait cette méthode.
Quand il s’agira de faire évoluer notre code en rajoutant, par exemple, une nouvelle fonctionnalité, le prix à payer sera moindre du fait que le code existant écrit “par intention” présentera l’avantage d’être clair (limitation des rôles de chaque méthodes par exemple).
Tests unitaires
Tous les bons développeurs vous le diront, plus une fonction est petite, avec une responsabilité limitée et claire, plus elle est facile à tester unitairement car on sait clairement ce que l’on doit tester (une seule fonctionnalité). Lorsque nous travaillons, par exemple, sur du code legacy, on est souvent confronté au fait que ce dernier soit codé de façon monolithique, avec notamment des fonctions à rallonge ayant plusieurs responsabilités : ce qui nous oblige dans un premier temps à faire du refactoring, et de scinder en plusieurs morceaux de traitement unique ce type de fonction afin de pouvoir tester unitairement chacune d’elle.
Conclusion
En règle générale, l’intention est toujours bonne au départ mais mettre en exergue son intention dans ses actes s’avère parfois être plus complexe. Un bon développeur doit probablement satisfaire en premier lieu le besoin de son client, mais il ne doit pas oublier qu’à coup sûr, un confrère (et lui même parfois) reprendra son code un jour. Ainsi, il devra penser à laisser derrière lui un code lisible, notamment et principalement dans son “intention”. En ayant à l’esprit la matérialisation de ses intentions dans son code, il en découlera tout un ensemble de règles et pratiques permettant une harmonisation de son programme et une meilleure qualité de code dans son ensemble : je pense notamment à des pratiques telles que le TDD, le Craft, le code review, etc. Pour finir, j’ajouterais qu’un code de qualité est un code lisible et simple à comprendre. Cependant, “la simplicité exigeant la perfection”, commençons donc par exprimer clairement nos intentions dans notre code. La suite est une autre histoire…
Quel bel article que je lis, que de chemins parcourus depuis 😉
Un excellent article. Il aborde de façon subtile l’intérêt très relatif à rendre un code réutilisable (vs le rendre le plus spécifique/clair possible), et des notions connues en DDD comme l’Ubiquitous Language qui énonce la nécessité d’aligner le vocabulaire de la solution sur la terminologie du métier (par exemple, j’ai noté que les méthodes de l’exemple sont en français…). Quelques patterns simples permettent aussi de rendre un code + clair sur ses intentions, par exemple le CQS… (http://martinfowler.com/bliki/CommandQuerySeparation.html)
Merci pour ton retour. Le sujet est vaste et ta remarque sur le CQS est intéressante !
Quelques coquilles à priori…
– dans MontantFrais(int x, int y, int z), je pense qu’il faut lire “montant = x + y;” et non y+z…
– dans CalculFraisFixes(int fraisTransport, int fraisforfaitaire) je pense qu’il faut lire “var fraisFixes = fraisTransport+ fraisforfaitaire;”…
Cordialement,
corrigé ! 😉