Préambule
Cet article est le second d’une liste de 5 chapitres, concernant l’architecture logicielle, afin de vous aider à analyser vos besoins, et à choisir l’architecture la plus adaptée pour réaliser votre projet.
- L’architecture Monolithique
- L’Architecture Orientée Service ou « Service Oriented Architecture (SOA) »
- L’Architecture Orientée Événement ou « Event Oriented Architecture (EOA) »
- Architecture Moderne : Les concepts essentiels
- Architecture Moderne : Exemple de création d’un nouveau projet
Lire l’article Architecture Logicielle – Chapitre 1 – L’Architecture Monolithique est recommandé pour une meilleure compréhension du présent article.
Introduction
Cette Architecture, très utilisée de nos jours, permet de résoudre un problème essentiel présent dans une architecture monolithique. En effet, une application monolithique finit toujours pas être impossible à maintenir à force d’évoluer. La complexité de l’application devient trop grande pour être gérée dans un seul et même programme, les technologies utilisées ne permettent pas toujours de traiter au mieux certaines fonctionnalités, et travailler à plusieurs peut devenir un véritable cauchemar.
L’architecture orientée service propose de séparer une application, en plusieurs composants/modules appelés “Services”. Ces services sont des programmes à part entière. Ils peuvent être développés avec n’importe quelles technologies, sans prendre en compte les technologies utilisées par les autres services.
Il s’agit d’une étape supplémentaire par rapport à la programmation orientée service. En effet, au lieu de séparer le code en plusieurs classes, on sépare l’application en plusieurs programmes.
Les services développés peuvent ensuite communiquer par l’intermédiaire d’APIs (ex: REST, SOAP, GraphQL, etc…). On utilise généralement HTTP pour mettre en place une API pour notre service, mais il est également possible d’utiliser d’autres systèmes permettant de transmettre des messages (ex: AMQP).
Les principaux avantages de cette architecture sont les suivants :
- Permettre une évolution constante (ajout de nouvelles fonctionnalités, refonte/mise à jour des modules obsolètes)
- Grande scalabilité (haute disponibilité)
- Plusieurs équipes peuvent travailler sur l’application en parallèle (répartition des services)
- Meilleure maintenance
- Possibilité de développer chaque services dans des technologies/versions différentes
Mais elle possède également des inconvénients :
- Plus complexe à mettre en place qu’une architecture monolithique (nécessite des connaissances plus poussées)
- Plus coûteuse sur le court terme (temps d’analyse et de mise en service plus conséquent)
Communication entre services
En Architecture Orientée Service, les services communiquent généralement via un système d’API. En effet, chaque service contient une partie des données de l’application.
Exemple : un service “Authentication” détiendrait l’entité “User” et “Group”, alors qu’un service “OrderManager” détiendrait l’entité “Order”. L’entité “Order” est liée à l’entité “User”. Cette liaison se traduit par la présence d’un “user_id” présent dans la table “order”. Mais la table “user” étant normalement dans une autre base de données (voir un autre système de gestion de base de données), on ne peut faire une requête SQL classique avec une jointure.
Les services, pour pouvoir travailler ensemble, et surtout permettre aux autres services d’accéder à leurs données, doivent définir une API. Très souvent, ces API reposent sur le protocole HTTP, afin de fournir des méthodes telles que “GET /user/{id}”, ou “POST /user”, etc… (exemples basés sur REST).
Ce type de communication, très courant, permet d’interroger un autre service, et obtenir une réponse. Néanmoins, il existe un autre type de communication consistant à envoyer un message à un/plusieurs services, sans attendre de réponse de leur part. Par exemple, si je souhaite envoyer un e-mail suite à l’inscription d’un utilisateur, je pourrais envoyer un message contenant les informations de l’e-mail (destinataire, objet, contenu, etc…), et considérer qu’un autre service traitera l’envoi de l’e-mail en temps voulu.
On retrouve ce principe dans des systèmes tels que AMQP, ou encore Kafka. Cela permet un traitement des messages en différés, et de pouvoir dupliquer les ressources visant à traiter les messages (plusieurs processus peuvent envoyer les e-mails en attente à la fois, en se répartissant les messages).
API Composition
L’API Composition est un Design Pattern relativement simple : Une API fait appel à d’autres API. On comprend tout de suite qu’il est indispensable en Architecture Orientée Service, car il s’agit tout simplement de la base de la communication dans cette Architecture.
Pour mettre en place ce design pattern, il faut que chaque service qui en fait parti définisse une API. Un service étant “propriétaire” de sa/ses bases de données, il devra fournir un moyen d’accéder à ses ressources aux autres services. Il s’agit tout simplement du principe d’encapsulation en programmation orientée objet (attributs privés, getters/setters publics).
Prenons comme exemple une application visant à générer des factures :
Le service “Billing” devra donc mettre en place une méthode permettant de récupérer, créer, modifier, supprimer des factures. Pour une API REST, cela ressemblerait à ceci :
- GET /bills
- GET /bill/{ID}
- POST /bill
- PUT /bill/{ID}
- DELETE /bill/{ID}
Le service “User” lui, définira les requêtes suivantes :
- GET /users
- GET /user/{ID}
- …
Avec cette solution, le service “Bill History”, pour afficher l’historique des factures, devra exécuter une requête HTTP vers le service “Billing” afin de récupérer toutes les factures, mais également vers le service “User” pour récupérer tous les utilisateurs (du moins ceux qui possèdent une facture).
Ensuite, il pourra afficher les factures, et les informations de l’utilisateur qui a été facturé.
API Gateway
Ce pattern est basé sur “l’API Composition”. Il répond à la problématique “Comment faire communiquer le Front avec plusieurs services ?”.
En effet, si on dispose de plusieurs services « back », chacun exposant sa propre API, laquelle pourrait être totalement différente d’un service à un autre (ex: REST, GraphQL, SOAP, AMQP, …), le travail nécessaire pour le Front deviendrait monstrueux. De plus, en permettant au Front de taper directement sur différents services, il faudrait alors que chacun de ces services mette en place une procédure de sécurité (ex: l’utilisateur est-il connecté ?). Cela reviendrait à une duplication de code, et enfreindrait la règle de “Responsabilité Unique”.
Une solution élégante est proposée grâce à ce design pattern. Il s’agit de définir un seul et unique service, accessible au Front. D’un point de vue “sécurité réseau”, cela est bien pratique puisqu’on pourrait restreindre les communications, et donc protéger tous les autres services d’un potentiel accès extérieur.
Ce service aura comme responsabilité “contrôler, traduire, rediriger, et agréger” :
- Contrôler : vérifier que l’utilisateur est connecté pour accéder à certaines API, ou bien administrateur pour accéder à d’autres, etc…
- Traduire : traduire la requête et la réponse d’un protocole/architecture à un autre
- Rediriger : agit comme un Reverse Proxy permettant de rediriger la requête sur le bon service
- Agréger : réunis des données issues de différents services, ou de différentes requêtes, à des fins d’optimisation
Si on compare l’API Gateway avec le patron d’architecture “MVC”, la responsabilité de l’API Gateway correspondrait essentiellement à celle du Contrôleur. Il agit en tant que véritable chef d’orchestre.
D’un point de vue “système”
Lorsqu’on adopte une architecture orientée service, il devient indispensable d’avoir une solution de déploiement performante. En effet, si l’on installe manuellement les serveurs, et les applications, alors le déploiement globale de la solution demanderait un temps considérable.
Aujourd’hui, une solution répond parfaitement au problème : La conteneurisation (ex: Docker).
Docker ne sera pas introduit plus en détails ici. Tout ce qu’il faut savoir, c’est que cette solution permet de définir un environnement adapté pour chaque service, et que le déploiement globale de la solution, peut-importe le nombre de service, peut être réalisé sur n’importe quel serveur (Linux/Windows/Mac) avec une seule commande. Si vous souhaitez en savoir plus, je vous invite à lire ce tutoriel.
Docker nous permet donc de développer des applications orientées services, destinées à être installées chez des clients, ou bien pour un fonctionnement interne.
En interne, les possibilités sont grandes : combiné à Kubernetes, il est possible de garantir une haute disponibilité, ou même de faire tourner plusieurs versions d’un même service en parallèle (de manière à permettre aux services dont il dépend d’évoluer progressivement par exemple).
Les services externes
Votre architecture orientée service se compose de services que vous gérez vous-même. Mais certains services peuvent être amenés à communiquer avec des services/prestataires externes (ex: Paiement via PayPal).
La communication (ou du moins la requête) peut se faire dans un sens, ou dans l’autre. Vous devrez donc consommer l’API du prestataire, ou fournir une API à votre prestataire. Cette API reposera sur des protocoles plus adaptés à des communications externes (ex: HTTP).
En soit, un service externe est similaire à un service interne : il s’agit d’une boîte noire, avec laquelle les autres services peuvent communiquer. La seule différence est qu’on n’a pas la main sur le code, et qu’il réside ailleurs que les services internes.
Exemple d’architecture orientée service
Avant d’aller plus loin, nous allons créer un exemple d’application basé sur une architecture orientée service. Le but de cette application sera d’envoyer des e-mails depuis une page Web. Sur cette page, nous aurons un formulaire permettant de saisir l’adresse mail du destinataire, l’objet, et le contenu du message. Un bouton “Envoyer” permettra de valider l’envoi, lequel sera confirmé par un message.
Formulaire d’envoi de mail depuis le service “MailBox”
Chaque nouvelle application/fonctionnalité nécessite d’être analysée avant de foncer tête baissée dans le code. En effet, rien de plus simple que de développer cette application, néanmoins il existe une infinité de solutions possibles.
Avec une architecture orientée service, nous commençons notre analyse en déterminant les différentes parties qui composent notre application. Dans le cas présent, nous avons une page Web (un formulaire basique), et un système d’envoi de mail.
Nous pouvons ensuite déterminer les langages et technologies que nous allons utiliser, en cherchant celles qui sont les plus adaptées pour traiter notre cas d’utilisation, et suivant les compétences que nous disposons. Si le seul langage “back” maîtrisé par l’équipe de développement est “PHP”, il serait ridicule de partir sur du Java par exemple, sauf si le choix d’une technologie non maîtrisée justifie une formation.
Dans notre exemple, nous choisissons donc PHP comme langage “Back”, et “HTML/CSS” pour le “Front”. Nous aurons également besoin d’un serveur Web (ex: Apache), et d’un serveur SMTP pour envoyer les mails.
Dans le cas d’une application monolithique, nous ferions tout d’un seul bloc. Et le projet serait bouclé en un temps record. Néanmoins, l’avenir de notre application serait limité. En effet, l’Architecture Orientée Service tend à nous faire factoriser, afin de pouvoir réutiliser chaque service au maximum.
Ainsi, nous pouvons développer un service “MailBox”, et un autre service “MailSender”. Le premier est une interface Web, permettant à n’importe quel utilisateur d’envoyer un mail, tandis que le second est un service “100% Back” permettant à n’importe quel service d’envoyer des e-mails.
La différence vient du fait que le premier se destine aux utilisateurs, et le second aux développeurs. Le deuxième service consistera en une API, permettant d’envoyer des e-mails. En terme de responsabilité, celui-ci aura en charge de bloquer les SPAMs, vérifier l’intégrité du mail, l’envoyer, etc…
L’organisation du développement est simplifiée en Architecture Orientée Service. En effet, il est tout à fait possible d’attribuer le développement de ces deux services à deux équipes distinctes :
- La première équipe, plus spécialisée dans le développement “front”, aura à charge la conception de l’interface utilisateur permettant de saisir l’e-mail.
- La deuxième équipe, plus spécialisée dans le développement “back”, aura à charge la conception d’un système d’envoi d’e-mail, et le développement d’une API permettant de le consommer.
Reste à définir le protocole utilisé pour communiquer entre ces deux services. Il existe de nombreuses possibilités, Mais pour rester simple dans cet exemple, nous utiliserons le très connu protocole HTTP (la communication se fera à travers un WebService, et il faudra donc installer un serveur Web comme Apache, avec le service “MailSender”).
L’API du service “MailSender” pourrait donc ressembler à ceci :
- POST “/send” JSON
- subject : L’objet du mail
- receiver : Destinataire du mail
- content : Le contenu du mail
Le service “MailBox” aura ainsi juste à exécuter une requête HTTP pour envoyer le mail, sans avoir à se préoccuper des spécificités d’un serveur mail, de l’analyse des SPAMs, etc…
Exemple d’utilisation en PHP :
// Endpoint du service "MailSender" pour l'envoi de mail
$url = 'http://mail-sender.dev/send';
$ch = curl_init($url);
// Paramètres de la requête au format JSON
$payload = json_encode([
'subject' => 'Hello World!',
'receiver' => 'jon.snow@winterfell.wes',
'content' => 'You know nothing Jon Snow!',
]);
// Configuration de la requête
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type:application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Exécution de la requête
$result = curl_exec($ch);
curl_close($ch);
Bien sûr, on préférera développer une/des classes permettant d’encapsuler cette logique. Dans l’idéal, une interface “MailSenderInterface” possédant une méthode “send”, et une implémentation “MailSender” avec le code ci-dessus.
Conclusion
Cette architecture est plus complexe et plus lourde qu’une architecture monolithique. En effet, nous sommes obligés de développer une fonctionnalité supplémentaire : l’API.
Le développement est plus long, et le déploiement également. Il faut installer PHP et un serveur Apache pour les deux services. Par contre, ce que nous avons fait est réutilisable. Ce qui signifie que l’ajout d’une nouvelle fonctionnalité/application, même si elle n’a rien à voir avec “MailBox”, pourra envoyer très facilement des e-mails. Supposons par exemple que l’on veuille créer un système d’alerte par e-mail lorsque le taux du Bitcoin augmente. Cette application pourrait se concentrer sur ce qui la rend unique : la surveillance des taux du Bitcoin. Mais elle pourrait profiter du service d’envoi d’e-mail, et son utilisation se résumerait au morceau de code vu plus haut. Cela revient à la même chose que de créer une bibliothèque, mais en plus puissant puisque l’utilisation du service, contrairement à une bibliothèque, peut se faire depuis n’importe quelle application (du moment qu’elle supporte le protocole de communication), sans installation supplémentaire. L’utilisation d’un service n’impose pas de système d’exploitation, de langage de programmation, de bibliothèque, etc…
En termes d’évolutivité, ce choix est parfait. Dans le cas où le format des adresses e-mails évolue (prise en compte de nouveaux caractères spéciaux par exemple), il suffira de mettre à jour le service “MailSender”, sans avoir à modifier chaque application indépendamment.
La maintenance est également facilité, puisqu’une équipe spécialisée dans les envois d’e-mails pourra prendre en charge le service, et corriger les bugs en se concentrant sur ce que fait le service uniquement. Même le changement de version de PHP n’aurait aucun impact sur les autres services.
En général, les services composent une application plus globale. C’est-à-dire qu’on ne crée pas un service uniquement dans le cas où celui-ci pourrait être utilisé dans le contexte d’une autre application totalement distincte. C’est d’ailleurs rarement le cas. On cherche plutôt à découper notre application en plusieurs composants, identifiables facilement, afin de séparer la logique et les responsabilités de l’application.
Aller au Chapitre 3 – L’Architecture Orientée Événement ou « Event Oriented Architecture (EOA) »