114 lines
8.3 KiB
Plaintext
114 lines
8.3 KiB
Plaintext
|
== 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*.
|