Blog Arolla

Patterns et divination

Ce keynote de Kent Beck à DDD Europe 2023 parle évidemment de son livre Tidy First, mais surtout de design de code et de dépendances. Il m’a donné plusieurs nouvelles billes sur ce qu’est le couplage. Le couplage et la cohésion sont la particule élémentaire de l’architecture. Et pourtant, j’ai toujours eu du mal à mettre des mots sur tous les détails que ces concepts représentent. Après avoir vu ce talk, j’ai l’impression de boucher la plupart des manques. Je vous propose de les visiter ensemble.

Tout d’abord, dans les 2 cas, on parle de cascades de modifications de code. On doit modifier un morceau de code en conséquence d’une autre modification souhaitée. Dans le jargon, on appelle ça une dépendance. Couplage et cohésion sont des formes de dépendances.

Quand la dépendance est proche, c'est à dire que le code à modifier en aval est proche du code à modifier en amont, c’est bien, et on appelle ça cohésion. Quand la dépendance est distante, on appelle ça couplage, et c’est mal. On veut pouvoir trouver rapidement les conséquences de nos modifications, en ayant tout le code impacté sous les yeux. C’est dans la cohésion, et pas le couplage, qu’on trouve l’évolutivité du code, sa capacité à être modifié en minimisant les effets de bord.

Et maintenant, le twist ultime que Kent Beck ajoute dans ce talk, ou en tout cas que j’y ai découvert, c’est que le couplage ou la cohésion se jugent à l’aune d’un changement à venir. Pour une même base de code, en fonction de la modification à apporter, la dépendance peut être proche ou distante, nombreuse, unique, ou même inexistante. Bref, le code, au regard de la modification future, peut être couplé ou cohésif.

Pourtant, quand on conçoit notre code pour lui apporter le plus de flexibilité, on le fait de manière à en maximiser la cohésion et en minimiser le couplage. Or, comme on vient de le voir, cela n’a de sens que si on connaît la modification à apporter au code. C’est à dire en prédisant l’avenir. Et on ne sait le faire qu’en faisant des allers-retours dans le temps. A la limite, on peut prédire le prochain coup, mais pas les 5 prochains. Autrement dit, sans boule de cristal, on ne sait pas quel design est le meilleur.

Illustrons ça avec le moteur d’un jeu. Les jeux c’est rigolo. Explorons un prototype dans lequel nous avons des personnages, qui peuvent être une sportive ou un nerd, et qui peuvent attendre ou avancer. Ces personnages utilisent leur énergie pour se déplacer :

sealed class Personnage(open var patate: Int) { 
    abstract fun avance() 
    abstract fun attends() 
}

class Nerd(override var patate: Int) : Personnage(patate) { 
    override fun avance() { 
        patate -= 5 
    }

    override fun attends() { 
        patate += 2 
    } 
}

class Sportive(override var patate: Int) : Personnage(patate) { 
    override fun avance() { 
        patate -= 2 
    }

    override fun attends() { 
        patate += 3 
    } 
}

Pour cela, nous allons appliquer des commandes:

abstract class Commande(val personnage: Personnage) { 
    abstract fun applique() 
}

class Avance(personnage: Personnage) : Commande(personnage) { 
    override fun applique() { 
        personnage.avance() 
    } 
}

class Attends(personnage: Personnage) : Commande(personnage) { 
    override fun applique() { 
        personnage.attends() 
    } 
}

Et enfin, faire gérer une série de commandes par le jeu :

class Jeu { 
    fun joue() { 
        val antoine = Nerd(10) 
        val ledecky = Sportive(10) 
        val commandes = listOf(Avance(ledecky), Avance(antoine), Avance(ledecky), Attends(antoine)) 
        commandes.forEach { it.applique() } 
    } 
}

Dans cette implémentation, il est facile d’ajouter un type de personnage. Si je veux ajouter un canard, par exemple, j’ajoute une classe Canard qui implémente la classe mère Personnage. Basique.

Maintenant, je vois bien que jouer un nerd est ingérable. Ajoutons donc des types d’actions, comme un vrai repos, une avance rapide, ou la dissimulation en position fœtale pour rendre le jeu un peu plus équilibré. Le design actuel va me gêner pour atteindre ce but, parce qu’il m’oblige à modifier tous les personnages pour ajouter une action. Les commandes et les personnages sont couplés au regard de l’ajout d’action. Avant de faire cet ajout, je vais donc suivre les préceptes de Kent Beck, et rendre la modification facile avant de faire la modification facile. Je vais refactorer mon code pour déplacer la gestion de l’énergie dans la classe commande :

sealed class Personnage(open var patate: Int) 
class Nerd(override var patate: Int) : Personnage(patate) 
class Sportive(override var patate: Int) : Personnage(patate) 
class Canard(override var patate: Int) : Personnage(patate)

abstract class Commande(val personnage: Personnage) { 
    abstract fun applique() 
}

class Avance(personnage: Personnage) : Commande(personnage) { 
    override fun applique() { 
        when (personnage) { 
            is Nerd -> personnage.patate -= 5 
            is Sportive -> personnage.patate -= 2 
            is Canard -> personnage.patate -= 1 
        } 
    } 
}

class Attends(personnage: Personnage) : Commande(personnage) { 
    override fun applique() { 
        when (personnage) { 
            is Nerd -> personnage.patate += 2 
            is Sportive -> personnage.patate += 3 
            is Canard -> personnage.patate += 2 
        } 
    } 
}

Avec cette modification, ajouter des actions ne consiste plus qu’à ajouter des implémentations de Commande. Cohésion. Simple.

Et si après ça je veux ajouter le personnage ver des sables ? Le nouveau design a rendu les personnages et les commandes couplés au regard de l’ajout de personnage. Je refactore pour ramener la gestion des actions dans les implémentations de personnage ? Je tournerais en rond, ni simple ni basique. Il va falloir que je trouve un design qui rende les 2 types de modifications faciles.

Et tout ça était évidemment simpliste. Je n’avais que 2 dimensions, 2 degrés de libertés, dont l'un était rigidifié et l’autre lubrifié. Et si maintenant je veux ajouter le mode de locomotion (un canard peut voler, marcher, ou nager) ? Et le terrain (depuis le film Tremors on sait qu’un ver des sables ne peut pas progresser dans la roche) ?

Tout ça pour dire qu’on ne peut pas prévoir un design parfait tout court, ceci pour 2 raisons :

  • On ne peut évaluer l’évolutivité d’un code qu’au regard des modifications qu’on va lui apporter.
  • On ne sait pas prédire l’avenir.

Mais alors comment écrire du code résistant à l’avenir ? En améliorant notre capacité à estimer, à prédire l’avenir, évidemment. Je rigole bien sûr. Dans un environnement complexe, la réponse n’est pas de mieux prédire, mais de mieux répondre.

En l’occurrence, dans le code, il s’agit d’optimiser la plasticité. La technique la plus directe, c’est de s’approcher du graal du code modifiable à l’envi : le green field. C’est à dire le code qui n’existe pas, le fichier vide, 0 ligne de code. Il faut tendre vers ce vide idéal, en gardant toujours le moins de code possible sous la main.

Vous me direz qu’on ne peut pas limiter notre système à 200 lignes de code, et vous aurez tout à fait raison. On touche là à l'art de l'architecture sans la prétention du pouvoir de divination, et j’espère que nous aurons l’occasion d’y réfléchir prochainement. En attendant, restons humble et gardons le code aussi simple que le domaine.

Ecartons-nous maintenant légèrement du talk de Kent Beck. On l’a effleuré dans cet article, on peut aussi parler d’un autre savoir-faire indispensable à son évolution. L’évolutivité d’un code est fonction de son design et de ses évolutions futures, on l’a vu. Mais également de la relation entre le code et les devs qui le modifient. Plus les devs ont le savoir-faire pour modifier ce code de manière maîtrisée, plus le code sera en mesure d’évoluer. Ça ressemble à une lapalissade, et pourtant ça ne semble pas être si évident que ça dans la nature. Sinon arolla n’aurait pas besoin de proposer la magnifique formation Working with Legacy Code au ROI défiant toute concurrence. Et son contenu ne serait pas une découverte pour les stagiaires dont la carrière en ressort bouleversée.

L’équipe doit être capable :

  • D'écrire et maintenir les fonctionnalités et propriétés du code sans effets de bord indésirables,
  • De faire évoluer le design du code sans en modifier les fonctionnalités et propriétés,
  • D'écrire et maintenir un harnais de confiance autour du code (check, déploiement, monitoring, test, tout ça en continu), pour vérifier au plus tôt qu’on n’en a pas diminué la valeur.

Et on l’oublie souvent : l’équipe doit être consciente qu’elle sait faire tout ça. Combien de fois ai-je vu des devs bloqué·e·s pour faire évoluer un bout de code alors qu’elles et ils étaient capables d’en modifier la structure pour y rentrer les modifications fonctionnelles promises ? Avec l’expérience, non seulement on apprend à refactorer, mais on apprend aussi à savoir qu’on est capable de refactorer. On apprend aussi, d’ailleurs, à savoir quand le contexte nous permet de refactorer avec confiance ou pas.

Et voilà, nous sommes équipés pour la base du code. À partir du moment où on a écrit Hello World, le reste du cycle de vie du produit consiste à le modifier. Alors on le garde petit et simple, et on s’assure de savoir le modifier, structurellement et fonctionnellement.

Plus de publications

Comments are closed.