Faciliter le passage à l’architecture microservices

Microservices

Image : http://comunytek.com/en/introduction-to-microservices/

Quand on regarde les tendances de l’industrie actuelle, il est difficile de ne pas entendre parler d’infonuagique (cloud) et des microservices. Depuis quelques années, des leaders comme Netflix, LinkedIn, Twitter, Google, Amazon et Microsoft se sont imposés dans ces domaines et ont bien voulu partager leur savoir-faire en publiant des plateformes open-source et des articles. Bien que l’univers du cloud et des microservices requiert des changements dans la manière de gérer les technologies et les infrastructures, il faut aussi adapter l’architecture logicielle pour aborder ces nouveaux paradigmes correctement.

Dans le contexte d’une application existante, le passage au cloud et à l’architecture microservices doit se faire de manière itérative. Il n’est pas judicieux de tout changer en même temps, car il devient impossible de mesurer si l’investissement aura eu les bénéfices escomptés. Alors, par où commencer?

Selon Sam Newman, la bonne pratique est de commencer par modifier la structure du code petit à petit afin de tendre vers l’indépendance des déploiements. Qui dit indépendance des déploiement dit évidemment découplage. Alors on doit commencer à créer des plus petites unités de déploiement dans notre logiciel qui soient découplées les unes des autres. Ce genre de changement peut se faire sans contrainte d’infrastructure et sans impact sur la sécurité. Voyons donc en détail comment on peut se préparer à l’approche microservice dès aujourd’hui.

Définir les frontières d’un service

Le principal défi au niveau du design dans l’architecture microservice est de déterminer les frontières d’un service. On sait que le monolithe n’est pas une bonne idée. On sait aussi qu’il n’est peut-être pas optimal de créer un service par classe de logique d’affaire (aussi appelé classe du domaine). Alors on doit trouver le juste milieu. Mais comment? Il n’y a pas de réponse toute faite à ce sujet, mais l’élément qui revient le plus souvent dans la littérature est d’utiliser le concept du Bounded Context tiré du Domain Driven Design. Celui-ci serait trop long à décrire dans cet article, mais l’idée est de s’assurer que notre service est lié à une seule fonction d’affaire. Par exemple, dans un site de commerce en ligne, les fonctions de gestion des commandes et de gestion des livraisons ne devraient pas faire partie du même service. En effet, pour faire un lien avec le Single-Responsibility-Principle (SRP) des principes SOLID, un service ne devrait avoir qu’une seule raison de changer. Normalement, les fonctions d’affaires tendent à rester relativement stables dans une entreprise. C’est sans doute l’élément qui changera le moins souvent. C’est donc une bonne idée de créer des services qui sont alignés avec ces fonctions d’affaires afin de minimiser les changements de frontières qui pourraient survenir. Toutefois, la connaissance de notre domaine d’affaire peut évoluer et amener à faire des changements dans la topologie des services. Le découpage et le niveau de découplage du code peut rendre ces changements très pénibles ou très faciles. Il est donc important de s’y attarder.

Le problème avec le découpage en couche

Pendant plusieurs années, l’architecture n-tiers proposait de séparer les composants logiciels par couche applicative.

N-Tiers

Cette façon de faire permet d’ordonner et de structurer le code pour séparer les responsabilités. Toutefois, elle n’est pas orientée vers les tests automatisés, car elle rend difficile la substitution et elle n’encourage pas le SRP en n’étant pas alignée sur les fonctions d’affaires. Il est aussi difficile d’éviter de faire un gros spaghetti et de comprendre d’un coup d’œil quel est le but de l’application. Lorsqu’un développeur veut créer un service avec une ou plusieurs classes de logique d’affaires, il doit effectuer une analyse des dépendances entre les classes afin de mesurer l’impact du changement. Cette approche rend plus difficile l’évolution de l’architecture.

Cohésion forte, couplage faible

En SOA, c’est sans doute le principe qui fait le plus consensus. Un service doit rassembler des opérations à forte cohésion. Tous les éléments d’un même sujet devraient être traités dans le même service. De plus, chaque service devrait être faiblement couplé avec ses pairs. Ainsi, on tend à diminuer les vagues de changements. C’est ce principe que reprend Uncle Bob dans son livre Clean Architecture. Il applique ce principe non seulement au niveau des services, mais aussi au niveau des composants. Il énonce certains principes permettant d’évaluer la cohésion et le couplage afin de créer les bons regroupements dans le code.

En DDD, le principe de cohésion forte encourage à regrouper les éléments du domaine qui sont intimement liés en agrégats. Toutefois, le couplage entre ces derniers doit être évité. On doit également éviter le couplage avec les technologies. L’interface utilisateur ou le moteur de base de données utilisé doivent être traités comme des détails d’implémentation qui sont jetables. Ce qui va perdurer dans le temps, comme mentionné précédemment, c’est la logique d’affaire et non la version de notre SQL Server. L’emphase doit être mis sur l’isolation de notre logique d’affaire. Elle doit être pure.

Pour ce faire, différentes approches ont été documentées dans le passé. Hexagonal Architecture (Port and Adapter), Onion Architecture et Clean Architecture sont toutes des variantes, mais partent du même principe. On doit inverser les dépendances. Pour ma part, je trouve que l’appellation « architecture en oignon » est celle qui parle le plus. On veut ajouter des couches autour de notre domaine. On représente nos couches de la manière suivante :

OnionArch

http://jeffreypalermo.com/blog/the-onion-architecture-part-1/

Le sens des dépendances doit toujours être vers le centre. Une fois qu’on a intégré ce paradigme dans notre code (merci aux IoC Containers), on obtient une architecture testable et plus modulaire. Toutefois, reste encore à trouver une manière de regrouper ces différentes couches en composants.

Découpage en composants adapté aux microservices

Si on résume, le but est d’isoler les classes de domaine afin qu’elles soient indépendantes des détails d’implémentation comme les bases de données et les composants UI. Le réflexe que j’ai eu dans le passé a été de créer un composant séparé pour chaque élément d’infrastructure (UI, Données, Cross-cutting concern). En .NET, c’est le seul mécanisme qui permet d’éviter à une classe du domaine de pouvoir instancier une classe d’accès aux données directement. Le fait de faire une référence vers le domaine dans le composant d’accès aux données empêche donc d’avoir une référence dans l’autre sens, et donc, le domaine ne peut avoir de dépendance directe inverse, ce qui créerait une référence circulaire. Toutefois, cette approche n’est pas parfaite : elle ne permet pas d’hurler l’architecture. On peut toujours s’en tirer avec une nomenclature de composant, mais ce n’est pas ce qui a de plus évident. Par ailleurs, s’en sont suivi deux phénomènes intéressants :

  1. Les composants UI ne sont pas obligés de passer par le domaine pour accéder aux données, ce qui nuit à la centralisation de la logique d’affaire
    1. En effet, nous avons vite vu apparaître des classes de la couche UI qui accédaient directement aux données.
  2. Lors d’une modification au domaine, on devait effectuer des changements dans plus d’un composant, ce qui montre que la cohésion n’était peut-être pas au maximum.

Pour moi, le dernier chapitre du livre Clean Architecture de Uncle Bob est sans doute le plus révélateur. Écrit par Simon Brown (auteur du framework C4), ce chapitre décrit une manière de regrouper les classes différemment. Il propose de regrouper dans un même composant les éléments suivants :

  1. Couche service (pas confondre avec le projet Web API ou WCF qui sont plus du UI d’une certaine façon)
  2. Accès aux données

Dans ce composant, la seule chose qui demeure publique est la porte d’entrée du composant. On obtient une architecture comme celle qui suit :

MSFinal

(En pâle, les types qui sont internes)

Brown propose de tirer profit du compilateur pour faire respecter l’architecture en rendant internal les éléments d’accès aux données et les classes du domaine. On obtient une architecture plus parlante. Juste en regardant les composants on comprends où se trouve la gestion des commandes. Aussi, le découplage demeure inchangé. Par ailleurs, il devient facile de déplacer un concept d’un service vers un autre. On vient ainsi se donner de la flexibilité quand vient le temps de revoir le découpage des services en approche microservices. Pas d’analyse d’impact très longue, tout est dans le composant. C’est une façon d’adapter notre architecture en prévision d’une migration vers les microservices.

Les plus malins auront toutefois noté qu’il devient plus facile d’instancier une classe d’infrastructure et donc de ne pas respecter le sens des dépendances. C’est en effet un compromis, mais puisque ce problème ne peut pas se propager à l’extérieur du composant, on peut découvrir les fautes et le redressement sera en général plus rapide. On y gagne toutefois une plus forte cohésion et on évite de disperser la logique d’affaire.

Comme ces composants sont très autonomes, il devient facile de déployer ces derniers de différentes façons. L’évolution d’un composant comme celui-là peut être confié à une équipe indépendante étant donné qu’il a très peu de dépendances externes.

Conclusion

Même si l’adoption de l’approche microservice comporte de nombreux enjeux, il est possible de commencer dès aujourd’hui à se préparer dans notre code patrimonial. Une fois l’indépendance du code obtenu, il faut s’attaquer aux bases de données, ce qui n’est pas une mince affaire non plus. Comme la route est longue, ça nous force à travailler là où les gains sont les plus nécessaires. L’architecture microservice n’est pas une destination, mais plutôt un outil qu’on doit utiliser pour atteindre des objectifs spécifiques.

Dette technique – Partie 1

Comme plusieurs développeurs, j’ai toujours préféré travailler dans un contexte de nouveau développement et non en maintenance ou en entretien. Pour moi, ces deux derniers termes étaient péjoratifs parce qu’ils étaient synonymes de « patch » et de « workaround », voire de frustration. Un logiciel qui dégénère semblait être à cette époque une évolution normale et une fatalité. Après quelques années de bon service, on devait prévoir une réécriture. Ça ne donnait pas vraiment le goût à quiconque de travailler dans un contexte semblable. Produire quelque chose en sachant qu’on prévoit déjà le jeter n’est pas très motivant.

Pourquoi fallait-il toujours en venir à ça? Qu’est-ce qui fait que les logiciels vieillissent souvent mal? Malheureusement, il n’y a pas consensus dans l’industrie sur la manière de nommer les éléments qui impactent négativement le vieillissement d’une application. Certains vont parler de santé d’un système, d’autres de qualités non-fonctionnelles ou encore de qualité tout court. Un terme à la mode depuis quelques années, surtout en agilité, pour parler des éléments techniques qui sont problématiques, est celui de la dette technique. Je vous propose de creuser un peu le sujet pour voir ce qui en retourne.
(Temps de lecture 10 minutes)

Méthaphore

La métaphore de la dette technique a été introduite par M. Ward Cunningham en 1992 :

Shipping first time code is like going into debt. A little debt speeds development so long as it is paid back promptly with a rewrite… The danger occurs when the debt is not repaid. Every minute spent on not-quite-right code counts as interest on that debt. Entire engineering organizations can be brought to a stand-still under the debt load of an unconsolidated implementation, object-oriented or otherwise.

Si on paraphrase, la notion de dette technique fait le parallèle entre la notion de dette financière et le développement logiciel. Si on livre du code qui n’est pas de première qualité, on aura à le refaire éventuellement (rapidement svp!) et ça aura donc coûté plus cher à développer au final (capital + intérêts). En fait, tout le temps supplémentaire effectué sur ce code de pietre qualité doit être considéré comme des intérêts (difficulté de lecture, maintenance compliquée, etc.).

Une notion de stratégie est aussi sous-jacente sans quoi on parlerait simplement de mauvais code. Dans la définition de M. Cunningham, contracter une dette technique peut être une bonne chose si un autre objectif est atteint pour compenser. Le gain peut être le délai de commercialisation ou time to market plus court, rétroaction plus rapide ou autre.

Cette métaphore ciblait donc essentiellement du code, ce qui ne semble plus tout à fait être le cas aujourd’hui. La notion de dette technique a évolué et on y inclut maintenant à peu près tout ce qui n’est pas idéal du point de vue technique. On peut donc parler de dette de code, de connaissance, de test, d’architecture et de dette technologique. Voyons un peu les différents types de dette.

 

Types de dette

Dette de code

La dette de code regroupe tous les éléments reliés directement au code. Les avertissements du compilateurs, les indices de maintenabilité ou de complexité cyclomatique, longueur des méthodes ou des classes, etc.. D’autres éléments sont toutefois plus difficile à déceler. Quelle sont les qualités non-fonctionnelles que du code devrait avoir? Facile à modifier, couplage faible, respect des principes SOLID, facile à lire, représentatif du domaine d’affaire, etc.. Dépendamment du contexte, ces éléments sont d’une importance relative, ce qui explique probablement qu’il n’y a pas encore de consensus…

Dette de connaissance

Est-ce que votre documentation est à jour en tout point? Probablement pas… Il ne faut pas s’en vouloir, je n’ai pas encore vu une seule organisation où c’était le cas. Ce qui varie, c’est le degré de fiabilité de cette dernière. Si les documents ne sont pas à jour ou sont incomplets, comment fait-on pour se retrouver? Il faut demander au super-héros qui était là lors du dernier projet? Espérons qu’il a une bonne mémoire… Et si on parlait de PowerBuilder, VB6 et les autres? Par ailleurs, êtes-vous certains d’avoir bien compris le domaine d’affaire? Il suffit de prendre deux experts du domaine d’affaire et de les faire discuter ensemble pour se rendre compte que les termes employés sont les mêmes, mais que la définition est différente d’une personne à l’autre.

Dette de test

Le manque de couverture par les tests automatisés est une dette incontestable. Une application vieillira mal si on ne peut pas valider facilement et rapidement les changements qu’on y fait. On peut toujours s’en sortir avec les tests manuels, mais ils sont coûteux, lents et ils ne sont pas infaillibles. Par ailleurs, qui est capable de savoir ce qu’il faut tester lorsqu’on change la classe Dieu (God class)? La majorité des équipes ne prennent pas de chance et test l’application au complet. L’autre option est de ne pas modifier l’application…

Dette d’architecture

Cette notion de dette est plus difficile à cerner étant donné qu’il n’existe pas de principes architecturaux clairs et réglementés en développement logiciel comme c’est le cas pour les bâtiments. Un conseiller en architecture pourra toujours dire que le couplage est acceptable pour le moment étant donné les besoins actuels et, comme il est difficile de prévoir les changements, l’architecture est donc correcte. Ce dernier aura raison, mais c’est un jeu risqué. Par ailleurs, certains choix architecturaux pourront être fait en début de projet et, s’ils ne sont pas remis en question, il faudra vivre avec les intérêts de la dette ainsi contractée. C’est souvent cette étape qui fait mal en maintenance ou en entretien de système. On évite des changements fondamentaux pour ne pas déstabiliser l’application. Mais c’est une erreur parce qu’on commence alors à ajouter des nouvelles classes avec des noms proches de celles qui existent déjà et on ajoute des condition à un if else if qui n’en finit plus et on rend le code plus difficile à modifier, car on ajoute de la complexité.

Dette technologique

La dette technologique est plus facile à comprendre à mon avis. Toutes les librairies ont une version. Même chose pour les systèmes d’exploitation et les serveurs de base de données. Ne rien faire lors de la sortie d’une nouvelle version contribue à accroître la dette. On doit mettre à jour les technologies sur lesquelles reposent nos applications, sinon on doit gérer le risque qu’on encoure. Par ailleurs, mette à jour une version à la fois d’un produit est souvent moins coûteux que de passer trois versions en même temps.

 

Pourquoi avons-nous de la dette technique?

Selon la définition de M. Cunningham, la réponse est simple : c’est voulu. C’est une décision d’équipe qui est éclairée. Avec la définition élargie dont on parle maintenant, les raisons sont plus subtiles. La raison qui revient le plus souvent est celle de la pression de la date de livraison. On devait livrer alors on a coupé les coins ronds pour y arriver. Cependant, un changement externe à notre application peut engendrer de la dette technique. Le changement de version d’un API en est un exemple, mais ce n’est pas toujours aussi facile à détecter. Par ailleurs, il faut convaincre les clients qu’il doivent investir pour payer la dette. Certains clients sont plus coriaces que d’autres… Pourtant, la dette ne s’éliminera pas toute seule.

Votre logiciel n’est peut-être pas facilement modifiable. Si la couverture de code par les tests est insuffisante ou inexistante, on voudra limiter les changements ou les faire en périphérie, ce qui aura pour effet de contrevenir à l’évolution du système ou de l’application et à ralentir la maintenance.

L’obsession des nouvelles fonctionnalité n’est pas à négliger. Certains clients qui veulent toujours plus de fonctionnalités sans retravailler l’existant s’expose à des problèmes. Il ne faut pas mal interpréter l’agilité ou le SCRUM. Il est vrai que le but est toujours de produire de la valeur pour le client à chaque itération. Mais il faut s’habituer à garder une portion du budget pour l’entretien et le refactoring pour avoir une saine évolution.

 

Et ensuite?

Un fois qu’on comprend bien les types de dette, il importe de s’en occuper. Pour ce faire, on devra tenter de la repérer, de la mesurer et d’y remédier. C’est ce que nous verrons dans un prochain billet.