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.