Orthogonalité
L’orthogonalité est un concept essentiel si l’on veut produire des systèmes faciles à concevoir, à construire, à tester et à étendre. Cependant, le concept d’orthogonalité est rarement enseigné directement. Il s’agit souvent d’une caractéristique implicite de diverses autres méthodes et techniques que vous apprenez. C’est une erreur. Une fois que vous aurez appris à appliquer directement le principe d’orthogonalité, vous constaterez une amélioration immédiate de la qualité des systèmes que vous produisez.
— Dave Thomas, Andy Hunt. « The Pragmatic Programmer »
Ainsi commence le chapitre 10 du livre « The Pragmatic Programmer » de Dave Thomas & Andy Hunt. Excellent livre dont nous avons déjà parlé dans les articles Duplication ou coïncidence ? et L’art de l’aiguisage. Aujourd’hui nous allons donc parler d’orthogonalité, de ce qui se cache derrière cette notion, et des bénéfices que peuvent nous apporter sa compréhension et sa mise en pratique !
Qu’est-ce que l’orthogonalité ?
Il s’agit d’une analogie avec les vecteurs orthogonaux en algèbre linéaire : aucun des vecteurs d’un ensemble orthogonal ne dépend des autres et tous sont nécessaires pour décrire l’espace vectoriel dans son ensemble.
L’orthogonalité signifie que les caractéristiques peuvent être utilisées dans n’importe quelle combinaison, que les combinaisons ont toutes un sens et que la signification d’une caractéristique donnée est cohérente, indépendamment des autres caractéristiques avec lesquelles elle est combinée.
— Michael Scott. « Programming Language Pragmatics »
Ainsi, lorsqu’on parle d’orthogonalité en ingénierie logicielle, on fait référence aux notions de découplage, mais aussi de composabilité, de prédictibilité et de cohérence.
Limiter les effets de bord
Penser en termes d’orthogonalité nous amène à concevoir des composants respectant les principes SOLID promus par Robert C. Martin. On va leur octroyer une responsabilité unique, favoriser l’injection de dépendance, ou encore les rendre déterministes, limitant ainsi les effets de bord.
Ce faisant, il est bien plus simple de tester nos composants puisque leur périmètre est restreint au strict nécessaire ; leur comportement prédictible, car ils sont insensibles aux perturbations exogènes ; et leurs dépendances quasi inexistantes, il sera donc plus aisé de mettre sur pied un jeu de tests de façon à reproduire les différents scénarios pouvant se présenter.
Non seulement nos tests sont plus simples à écrire, mais nos composants, découplés les uns des autres, sont aussi plus facilement modifiables, adaptables, voire remplaçables !
Effets algébriques
Une approche pratique pour contenir les effets de bord nous vient de la programmation fonctionnelle et se nomme effet algébrique.
Les effets algébriques sont une approche des effets computationnels basée sur le principe que le comportement impur d’une fonction découle d’un ensemble d’opérations telles que get & set pour le stockage mutable, read & print pour les entrées/sorties interactives, ou raise pour les exceptions. Cela donne naturellement lieu à des handlers non seulement pour les exceptions, mais aussi pour tout autre effet qui, entre autres, peut capturer la redirection de flux, la rétrospection, le multithreading coopératif et les continuations délimitées.
— Matija Pretnar. « An Introduction to Algebraic Effects and Handlers »
Une implémentation de cette approche existe en Ruby via la gem dry-effects qui, pour ne rien gâcher, est livrée avec une documentation fournie et éclairante sur son potentiel.
Gagner en productivité
Bien qu’assez exigeant et demandant un peu d’exercice, penser en termes d’orthogonalité peut assez vite s’avérer payant. En effet, l’empreinte de nos composants étant réduite, y apporter des modifications en sera d’autant plus aisé. Car il va de soi que sans couplage ni effet de bord, nous serons plus facilement enclins à réaliser des changements que nous saurons localisés. Sérénité et productivité vont de pair !
Par ailleurs, il nous sera aussi très facile de chaîner nos composants pour répondre à des besoins plus complexes. En permettant la composabilité, l’orthogonalité favorise ainsi la réutilisabilité de nos composants. Sans cela, nous ferions face à des composants dont les responsabilités se chevauchent, voire à des incompatibilités structurelles.
Composabilité
En Ruby, la gem dry-transformer, dont nous avons déjà parlé ici-même pour présenter une manière élégante de manipuler des structures de données, est un exemple de méthodes simples et composables pour couvrir un grand nombre de scénarios.
Réduire les risques
Restreindre le périmètre, les responsabilités et les dépendances nous permet aussi d’éviter l’effet domino ! Un composant risque moins de propager un comportement inattendu s’il est isolé des autres et n’a pas d’effet de bord.
De la même manière, dans un système orthogonal il devient assez aisé de substituer une dépendance à une autre. Changer d’ORM, de SGBD, voire de framework devrait être du domaine du possible — moyennant un certain effort, voire un effort certain, mais néanmoins possible — et idéalement n’avoir aucun impact sur notre code métier.
Une architecture résiliente
Sans forcément l’avoir pensé en ces termes précis, nous sommes déjà habitués à concevoir des systèmes orthogonaux. Le découpage en 7 couches du modèle OSI en est un exemple, l’architecture hexagonale en est un autre, ou encore les microservices pour en prendre un troisième. Toutes ces architectures ont en commun, sans jamais vraiment l’exprimer en ces termes, une recherche d’orthogonalité.
Ainsi, une architecture orthogonale sera résiliente, dans le sens où sa capacité à absorber un changement sera élevée et que cela ne génèrera pas de soubresauts à travers toute l’application.
Du choix de ses outils
L’art de l’aiguisage nous l’a appris, le choix de ses outils est primordial pour qui recherche précision et efficacité. Ainsi, choisir des bibliothèques ou autres greffons qui respectent le principe d’orthogonalité nous sera d’une grande aide pour la maintenabilité et la testabilité de nos applications. Les programmes GNU/Linux en sont un bon exemple, comme le fait remarquer Eric Steven Raymond dans l’extrait suivant de « The Art of Unix Programming ».
L’orthogonalité signifie également que plusieurs programmes n’ont pas les mêmes fonctions. Par exemple, sous GNU/Linux, la sélection raffinée de fichiers n’est effectuée que par le programme find. D’autre part, find ne peut que sélectionner des fichiers et n’a pas de fonctions supplémentaires ; il peut cependant être combiné avec toutes les autres commandes. Le programme tar peut combiner plusieurs fichiers en une archive ; pour la compression, il est combiné avec gzip. gzip ne peut compresser qu’un seul fichier et ne peut ni sélectionner ni combiner des fichiers.
— Eric Steven Raymond. « The Art of Unix Programming »
Un code découplé
Alors comment concevoir de tels programmes ? Au quotidien, il est difficile de conserver l’orthogonalité de son code sans y apporter une attention particulière. Nos frameworks ne nous y encourage pas toujours non plus, ce qui n’arrange rien. Cependant, et sans dénaturer l’esprit et la philosophie du framework que l’on utilise, nous pouvons tâcher de préserver un découplage quand l’occasion nous en est donnée. Il est par exemple préférable de laisser un objet gérer lui-même son état interne. Limiter l’exposition de nos objets sur l’extérieur est là aussi une pratique qui va dans le sens recherché.
Prenons un exemple inspiré du livre de Dave Thomas & Andy Hunt. Selon vous, laquelle de ces deux classes respecte le mieux le principe d’orthogonalité ?
class Split1
def initialize(fileName) # accède au fichier en lecture
def readNextLine() # lit la prochaine ligne
def getWord(n) # retourne le n-ième mot de la ligne courante
end
class Split2
def initialize(line) # découpe la ligne en mots
def getWord(n) # retourne le n-ième mot de la ligne courante
end
La seconde, en effet, puisqu’elle se concentre sur une seule tâche (découper une ligne en mots) sans s’inquiéter d’où provient cette ligne. Ainsi, non seulement on réduit le couplage, mais on améliore aussi sa composabilité, tout simplifiant l’écriture de tests.
Un code contextualisé
Inscrire son code dans un contexte est également un excellent moyen d’éviter les couplages non désirés, le cumul des responsabilités, et les effets de bord. En contextualisant son code, on s’évite l’usage de variables globales, partagées et modifiables par tous. Ce faisant, on s’offre aussi la possibilité de respecter le principe DRY ; lorsque nos méthodes et fonctions sont contextualisées, il est plus facile de discerner la duplication de la coïncidence.
DRY concerne la duplication de la connaissance, de l’intention. Il s’agit d’exprimer la même chose à deux endroits différents, peut-être de deux manières totalement différentes.
— Dave Thomas, Andy Hunt. « The Pragmatic Programmer »
Un code facile à tester
Avez-vous déjà eu à tester une méthode qui manipule toute une arborescence d’objets complexes, fait des appels en base de données et interroge une API externe, tout en étant sensible à une ou plusieurs variables d’environnement ? C’est une torture ! Et c’est là tout ce qu’on souhaite s’éviter en visant l’orthogonalité ! Réduire le nombre de dépendances et les inverser quand elles sont nécessaires permet une meilleure maitrise des cas de figures pouvant se présenter. Limiter l’exposition de nos objets réduit drastiquement le nombre de situations à tester. Favoriser la composabilité et le déterminisme de nos composants permet de faire face à un grand nombre de situations sans pour autant multiplier les tests.
Pour aller plus loin
Nous venons de le voir, cette notion d’orthogonalité se cache partout ! Et une fois identifiée, elle se révèlera être votre meilleure alliée pour concevoir des applications pérennes et résilientes.
Si vous souhaitez creuser davantage le sujet, je vous invite à la lecture de « The Pragmatic Programmer » de Dave Thomas & Andy Hunt ; ainsi que « The Art of Unix Programming » d’Eric Steven Raymond, dont le chapitre 4 aborde ce sujet, et plus largement la notion de modularité, du point de vue du développement applicatif sous Unix.