Une philosophie de la conception de logiciels

Ce billet date de plusieurs années, ses informations peuvent être devenues obsolètes.

A Philosophy of Software Design est un livre écrit par John Ousterhout et paru en 2018.

Fort d'une grande expérience du code mais aussi de beaucoup d'humilité, l'auteur donne des recommandations sur la conception et la gestion de la complexité des logiciels.

La philosophie en informatique a tendance à prendre la forme d'une énumération de maximes :

Dans ce livre, même si le style est concis, le propos va plus loin et chaque conseil est illustré (souvent en Java) et bien argumenté. On ne se cantonne pas non plus à des discussions vaines sur les seules méthodes ou techniques de programmation (impérative contre fonctionnelle etc.).

La majorité des recommandations sont entrées en résonance avec ce que j'ai appris au fil des années.

Ce bouquin est l'outil ultime pour faire une revue de code de qualité.

Voici mes notes de lecture.

Réduire la complexité

Les logiciels sont intrinsèquement plus complexes que les systèmes physiques.

En amont de la construction, il n'est pas possible de visualiser l'entièreté du design d'un logiciel pour en comprendre toutes les implications.

Par conséquent, le design initial aura de nombreux problèmes qui ne deviendront apparents que lorsque la mise en œuvre aura déjà bien avancée. La résolution de ces problèmes sans modifier le design global entraîne une explosion de la complexité.

En tant que développeur, il faut toujours être à l'affût des possibilités d'améliorer le design d'un système.

La réduction de la complexité est l'élément le plus important dans la conception de logiciels.

Il y a deux façons de combattre la complexité :

  • l'éliminer avec un code plus simple et plus évident
  • l'encapsuler de façon à pouvoir travailler sur un système sans être exposé à toute sa complexité grâce à un design modulaire (modular design)

Qu'est-ce que la complexité

La complexité logicielle peut se définir comme toute chose liée à la structure d'un logiciel qui en rend difficile la compréhension et la modification.

Elle se manifeste de 3 façons :

  • l'amplification d'un changement (change amplification) : un changement en apparence simple nécessite des modifications à beaucoup d'endroits
  • la charge cognitive (cognitive load) : quelle quantité d'information a besoin de connaître le développeur afin d'effectuer une tâche
  • les incertitudes inconnues (unknown unknowns) : quand il n'est pas évident de savoir quelle partie du code modifier pour réaliser une tâche

La complexité est causée par deux facteurs :

  • les dépendances : quand une partie du code ne peut être comprise ou modifiée en isolation
  • l'obscurité : quand une information importante n'est pas évidente

Les dépendances ne peuvent être complètement éliminées car elles sont une part fondamentale des logiciels. Par contre, la conception doit s'attacher à en réduire le nombre et à les rendre simples et évidentes.

La complexité apparaît avec le temps. Il est tentant de se convaincre qu'un petit changement apportant un peu de complexité n'est pas tellement grave. Mais si chaque développeur fait ça, la complexité s'accumule rapidement avant d'exploser.

Les recommandations de l'auteur

Adopter une façon de programmer stratégique plutôt que tactique. L'approche tactique consiste à obtenir quelque chose qui fonctionne le plus rapidement possible sans réfléchir au meilleur design possible. Malheureusement les contraintes budgétaires favorisent souvent une approche tactique.

Les modules doivent être plus profonds que large. Pour cela, l'interface d'un module doit cacher son implémentation. Les morceaux de connaissances qui représentent des choix de design sont embarqués dans l'implémentation du module mais n'apparaissent pas dans son interface. L'auteur parle de dissimulation d'information, c.f. On the criteria to be used in decomposing systems into modules.

Créer des modules à but générique plutôt que spécialisé. L'interface des modules doit être facile à utiliser pour le besoin actuel sans lui être spécifiquement lié afin de pouvoir être étendue.

Éviter les méthodes qui ne font que se passer des arguments entre elles, c'est le signe d'une mauvaise répartition de la responsabilité. Ajouter un élément dans une base de code doit permettre d'éliminer la complexité qui serait présente sans ce nouvel élément.

La décision de séparer ou de fusionner des modules doit être basée sur la complexité. Il faut choisir la structure qui engendre la meilleure dissimulation d'information, le moins de dépendances et les interfaces les plus profondes.

Imaginer deux façons différentes de faire le design. Vous finirez avec un design bien meilleur si vous considérez différentes possibilités pour chaque décision majeure.

Écrire des commentaires car ils permettent de capturer l'information qui était dans la tête du développeur mais qui est impossible à représenter par le code. Une bonne documentation peut permettre de réduire la charge cognitive, clarifier les dépendances et éliminer l'obscurité. Les commentaires devraient décrire des choses qui ne sont pas évidentes à la lecture du code. Il faut se mettre à la place du lecteur et se poser la question de savoir quelles seraient les informations clés dont il aurait besoin pour comprendre le code rapidement.

Bien nommer ses variables. Si un nom de variable est difficile à trouver, c'est probablement le signe d'un mauvais design. Idéalement un nom est explicite et rend le code évident. Il existe des opinions différentes comme celle d'un développeur du langage Go dont la préférence s'oriente vers des noms de variables courts.

Utiliser les commentaires dans le processus de design, c'est à dire les écrire préalablement au code.

Rendre le code plus évident pour en accélérer sa compréhension. Il est plus facile de remarquer que le code de quelqu'un d'autre n'est pas évident que de voir des problèmes avec son propre code. La revue de code est donc le meilleur étalon de l'évidence du code. Si quelqu'un lit votre code et ne le trouve pas évident, alors il y a de grandes chances que ça soit le cas.

L'utilisation judicieuse des espaces peut faciliter la lecture du code. À titre personnel c'est pour ça que j'aime Python.

Assurer l'uniformité de la base de code au sein d'une équipe via un coding style, des conventions, une même organisation des fichiers, etc.

Le Test-Driven Development est davantage adapté à la résolution de bugs. Sinon cette technique a tendance à focaliser l'attention sur l'obtention d'une fonctionnalité spécifique plutôt que de trouver le meilleur design possible.

Les tests unitaires permettent aux développeurs d'être plus confiants lors d'un refactoring.

Conclusion de l'auteur

Si vous améliorez vos compétences en matière de conception, non seulement vous produirez plus rapidement des logiciels de meilleure qualité, mais le processus de développement de logiciels sera plus agréable.

Avant OAuth et OpenID Connect Après Notes sur L'Art de PostgreSQL

Tag Kemar Joint