== Poésie de la programmation === Complexité de McCabe La https://fr.wikipedia.org/wiki/Nombre_cyclomatique[complexité cyclomatique] (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section. Quand le cycle d'exécution du code rencontre une condition, il peut soit rentrer dedans, soit passer directement à la suite. Par exemple: [source,python] ---- if True == False: pass # never happens # continue ... ---- TODO: faut vraiment reprendre un cas un peu plus lisible. Là, c'est naze. La condition existe, mais nous ne passerons jamais dedans. A l'inverse, le code suivant aura une complexité moisie à cause du nombre de conditions imbriquées: [source,python] ---- def compare(a, b, c, d, e): if a == b: if b == c: if c == d: if d == e: print('Yeah!') return 1 ---- Potentiellement, les tests unitaires qui seront nécessaires à couvrir tous les cas de figure seront au nombre de cinq: . le cas par défaut (a est différent de b, rien ne se passe), . le cas où `a` est égal à `b`, mais où `b` est différent de `c` . le cas où `a` est égal à `b`, `b` est égal à `c`, mais `c` est différent de `d` . le cas où `a` est égal à `b`, `b` est égal à `c`, `c` est égal à `d`, mais `d` est différent de `e` . le cas où `a` est égal à `b`, `b` est égal à `c`, `c` est égal à `d` et `d` est égal à `e` La complexité cyclomatique d'un bloc est évaluée sur base du nombre d'embranchements possibles; par défaut, sa valeur est de 1. Si nous rencontrons une condition, elle passera à 2, etc. Pour l'exemple ci-dessous, nous allons devoir vérifier au moins chacun des cas pour nous assurer que la couverture est complète. Nous devrions donc trouver: . Un test où rien de se passe (`a != b`) . Un test pour entrer dans la condition `a == b` . Un test pour entrer dans la condition `b == c` . Un test pour entrer dans la condition `c == d` . Un test pour entrer dans la condition `d == e` Nous avons donc bien besoin de minimum cinq tests pour couvrir l'entièreté des cas présentés. Le nombre de tests unitaires nécessaires à la couverture d'un bloc fonctionnel est au minimum égal à la complexité cyclomatique de ce bloc. Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil. Certains recommandent de le garder sous une complexité de 10; d'autres de 5. Il est important de noter que refactoriser un bloc pour en extraire une méthode n'améliorera pas la complexité cyclomatique globale de l'application. Nous visons ici une amélioration *locale*. === Conclusion [quote, Robert C. Martin] The primary cost of maintenance is in spelunking and risk cite:[clean_architecture(139)] En ayant connaissance de toutes les choses qui pourraient être modifiées par la suite, l’idée est de pousser le développement jusqu’au point où un service pourrait être nécessaire. A ce stade, l’architecture nécessitera des modifications, mais aura déjà intégré le fait que cette possibilité existe. Nous n’allons donc pas jusqu’au point où le service doit être créé (même s’il peut ne pas être nécessaire), ni à l’extrême au fait d’ignorer qu’un service pourrait être nécessaire, mais nous aboutissons à une forme de compromis. Une forme de comportement de Descartes, qui ne croit pas en Dieu, mais qui envisage quand même cette possibilité, ce qui lui ouvre le maximum de portes 🙃 Avec cette approche, les composants sont déjà découplés au niveau du code source, ce qui pourrait s’avérer suffisant jusqu’au stade où une modification ne pourra plus faire reculer l’échéance. En terme de découpe, les composants peuvent l’être aux niveaux suivants: * Code source * Déploiement, au travers de dll, jar, linked libraries, … voire au travers de threads ou de processus locaux. * Services Cette section se base sur deux ressources principales cite:[maintainable_software] cite:[clean_code], qui répartissent un ensemble de conseils parmi quatre niveaux de composants: * Les méthodes et fonctions * Les classes * Les composants * Et des conseils plus généraux. Ces conseils sont valables pour n'importe quel langage. ==== Au niveau des méthodes et fonctions * *Gardez vos méthodes/fonctions courtes*. Pas plus de 15 lignes, en comptant les commentaires. Des exceptions sont possibles, mais dans une certaine mesure uniquement (pas plus de 6.9% de plus de 60 lignes; pas plus de 22.3% de plus de 30 lignes, au plus 43.7% de plus de 15 lignes et au moins 56.3% en dessous de 15 lignes). Oui, c'est dur à tenir, mais faisable. * *Conserver une complexité de McCabe en dessous de 5*, c'est-à-dire avec quatre branches au maximum. A nouveau, si une méthode présente une complexité cyclomatique de 15, la séparer en 3 fonctions ayant chacune une complexité de 5 conservera la complexité globale à 15, mais rendra le code de chacune de ces méthodes plus lisible, plus maintenable. * *N'écrivez votre code qu'une seule fois: évitez les duplications, copie, etc.*, c'est juste mal: imaginez qu'un bug soit découvert dans une fonction; il devra alors être corrigé dans toutes les fonctions qui auront été copiées/collées. C'est aussi une forme de régression. * *Conservez de petites interfaces*. Quatre paramètres, pas plus. Au besoin, refactorisez certains paramètres dans une classe ou une structure, qui sera plus facile à tester. ==== Au niveau des classes * *Privilégiez un couplage faible entre vos classes*. Ceci n'est pas toujours possible, mais dans la mesure du possible, éclatez vos classes en fonction de leur domaine de compétences respectif. L'implémentation du service `UserNotificationsService` ne doit pas forcément se trouver embarqué dans une classe `UserService`. De même, pensez à passer par une interface (commune à plusieurs classes), afin d'ajouter une couche d'abstraction. La classe appellante n'aura alors que les méthodes offertes par l'interface comme points d'entrée. ==== Au niveau des composants * *Tout comme pour les classes, il faut conserver un couplage faible au niveau des composants* également. Une manière d'arriver à ce résultat est de conserver un nombre de points d'entrée restreint, et d'éviter qu'il ne soit possible de contacter trop facilement des couches séparées de l'architecture. Pour une architecture n-tiers par exemple, la couche d'abstraction à la base de données ne peut être connue que des services; sans cela, au bout de quelques semaines, n'importe quelle couche de présentation risque de contacter directement la base de données, "_juste parce qu'elle en a la possibilité_". Vous pourriez également passer par des interfaces, afin de réduire le nombre de points d'entrée connus par un composant externe (qui ne connaîtra par exemple que `IFileTransfer` avec ses méthodes `put` et `get`, et non pas les détails d'implémentation complet d'une classe `FtpFileTransfer` ou `SshFileTransfer`). * *Conserver un bon balancement au niveau des composants*: évitez qu'un composant **A** ne soit un énorme mastodonte, alors que le composant juste à côté ne soit capable que d'une action. De cette manière, les nouvelles fonctionnalités seront mieux réparties parmi les différents systèmes, et les responsabilités seront plus faciles à gérer. Un conseil est d'avoir un nombre de composants compris entre 6 et 12 (idéalement, 12), et que chacun de ces composants soit approximativement de même taille. ==== De manière plus générale * *Conserver une densité de code faible*: il n'est évidemment pas possible d'implémenter n'importe quelle nouvelle fonctionnalité en moins de 20 lignes de code; l'idée ici est que la réécriture du projet ne prenne pas plus de 20 hommes/mois. Pour cela, il faut (activement) passer du temps à réduire la taille du code existant: soit en faisant du refactoring (intensif?), soit en utilisant des librairies existantes, soit en explosant un système existant en plusieurs sous-systèmes communiquant entre eux. Mais surtout, en évitant de copier/coller bêtement du code existant. * *Automatiser les tests*, *ajouter un environnement d'intégration continue dès le début du projet* et *vérifier par des outils les points ci-dessus*.