Sommaire
Programmation Orientée Objet, Design/Architectural Patterns, … Vous en avez déjà entendu parler. Mais savez vous précisément ce en quoi ça consiste ?
Ces 8 articles vous permettront de comprendre les grands principes qui se cachent derrière ces termes techniques, mais également d’apprendre comment et pourquoi les utiliser.
- La Programmation Orientée Objet
- Introduction à UML : Diagrammes de classe
- Concepts de base de POO
- Les Patrons de Conception (Design Patterns)
- Les Patrons d’Architecture (Architectural Patterns)
- Concepts avancés de POO
- Qualité de Code
- Web Services
Introduction
Maintenant que vous maîtrisez parfaitement la Programmation Orientée Objet, et que vous avez bien compris ce que sont les Patrons de Conception/Architecture, et comment vous en servir, il est temps de découvrir des concepts un peu plus poussés, et de plus en plus utilisés dans le monde de la programmation.
Dans ce cours, nous aborderons des concepts tournant autour de ce qu’on appellera des “Services”. Le but étant de rendre notre application encore plus évolutive, maintenable, et réutilisable.
Injection de Dépendances
L’injection de dépendances est un Patron de Conception, comme ceux vus précédemment. Il appartient à la catégorie des Patrons de Création (bien que ne figurant pas dans les Patrons de Conception initialement définis par le Gang of Four), puisque son but est de gérer la façon dont des objets sont instanciés.
L’injection de dépendance fait partie de ce qu’on appelle “l’Inversion de Contrôle” (IoC : Inversion of Control). Ce principe est utilisé par les Frameworks, et correspond au principe d’Hollywood : “Ne nous appelez pas, c’est nous qui vous appellerons”. Cela signifie que c’est le Framework qui gère l’application, et non l’inverse. Nous avons dans ce cas inversé le contrôle.
Problème du Singleton
Vous vous souvenez de ce Design Pattern ? Il était bien pratique, et permettait d’accéder à une même instance partout dans l’application. Pourtant, il s’agit d’un Anti-Pattern. Au départ, il semblait intéressant, et séduisait tous les développeurs qui débutaient la Programmation Orientée Objet. Mais maintenant, nous savons que ce Pattern pose plusieurs problèmes.
Couplage
Le problème ? Des dépendances un peu partout dans le code, au lieu de passer par une interface, et donc difficile à tester.
Violation du principe de responsabilité unique
Une classe qui implémente le Singleton a, de ce fait, deux responsabilités : celle pour laquelle la classe a été prévue au départ, et la responsabilité de gérer son instanciation (puisque c’est un Singleton).
Cycle de vie
Avec un Singleton, on ne peut pas gérer le cycle de vie de la classe. Dans le cadre des Tests, cela est souvent problématique. En effet, l’instance est créée au premier appel, et détruite à la fin de l’exécution du programme.
Solution
Imaginez une classe, que nous nommerons “Container”, qui contient (via un fichier par exemple), la configuration de toutes nos classes qui auraient été des Singletons. Dans ce fichier de configuration, nous aurons plusieurs informations pour chaque classe :
- Un nom (définissant les fonctionnalités que la classe propose)
- Le nom complet de la classe (avec namespace/package)
- Ses dépendances (autres classes dont elle a besoin pour fonctionner)
Cette classe « Container » aurait une seule responsabilité : gérer l’instanciation de toutes nos classes qui auraient pu être des Singletons.
Arrêtons maintenant de parler de “Classes ayant pu être des Singletons”, et appelons les “Services”, d’accord ? Le but de ces Services est le même que pour les Singletons, sauf qu’ils n’auront qu’une seule responsabilité : Connexion à la base de données, Validation de données, Calculs, Envoi d’emails, etc…
Le Container est une sorte de patron “Fabrique” (souvenez vous, il gère lui-même l’instanciation des objets). En effet, le Container dispose d’une méthode (appelons là “get(name: string)”), qui retourne une instance du service dont le nom est passé en paramètre.
De cette manière, on peut gérer des instances uniques, et nous n’avons plus besoin d’ajouter un attribut “instance”, un constructeur privé, et une méthode “getInstance” à nos classes.
Les dépendances
Ce qui est génial avec l’Injection de Dépendances, c’est qu’on peut, grâce au fichier de configuration, changer rapidement les dépendances de chaque service. Comme le définit le concept d’Inversion of Control, ce n’est plus le service qui gère ses dépendances, mais le Container (“Ne nous appelez pas, c’est nous qui vous appellerons”). De cette manière, si on change de système de gestion de base de données, il suffirait d’une ligne à modifier pour que l’application fonctionne avec ce dernier.
C’est en fait assez simple à mettre en place. Si vous avez un Service A, qui a besoin d’un Service B pour fonctionner (car quelque part il fait appel à une méthode du Service B par exemple), on définit un constructeur pour notre Service A, qui prend en paramètre un Service B. C’est encore mieux de définir ce Service B par une interface, de sorte à pouvoir changer facilement le Service B qui est injecté dans le Service A.
Le Container, lui, va s’occuper de vérifier si le Service que nous souhaitons récupérer possède des dépendances. Si c’est le cas, il va, lors de l’instanciation du Service, passer à son constructeur les instances des autres Services dont il dépend. Si un de ces Service n’a pas encore été instancié, il va alors s’occuper de l’instancier (attention aux références circulaires : A dépend de B qui dépend de C qui dépend de A, car dans ce cas on tourne en rond à l’infini. Il existe bien sûr des solutions pour pallier à ce genre de situations).
Programmation Orientée Service
Rassurez-vous, la Programmation Orientée Service n’est pas un nouveau type de programmation comme a pu l’être la Programmation Orientée Objet par rapport à la programmation procédurale. Au contraire, il s’agit d’une manière d’utiliser la Programmation Orientée Objet pour éviter de recoder plusieurs fois la même fonctionnalité. Sur Internet, on trouve peu d’explications sur ce sujet, car souvent confondu avec l’Architecture Orientée Service. Il s’agit pourtant de deux choses différentes.
Vous avez vu plus haut en quoi consistait l’Injection de Dépendances. Et bien la Programmation Orientée Service est entièrement basée sur ce concept.
Souvenez vous de votre premier programme : une fonction, quelques centaines de lignes de code, aucune réutilisabilité. Puis vous avez compris l’importance des fonctions. Alors, vous avez commencé à factoriser votre code pour ne pas avoir à réécrire plusieurs fois la même fonctionnalité.
La Programmation Orientée Service, c’est pareil, sauf qu’on s’appuie sur l’Injection de Dépendances. La philosophie qui est derrière est toujours la même : factoriser. Mais avec l’Injection de Dépendances, on va pouvoir aller plus loin, et découpler.
Pour implémenter ce concept, considérez que la majorité du code de votre application doit résider dans des services, et que chaque service doit être le plus petit possible, ou du moins proposer des méthodes aussi unitaires que possible. Au final, on est assez proche de la création d’un maximum de fonctions réutilisables, sauf que nous avons maintenant accès à tous les avantages de la Programmation Orientée Objet.
L’intérêt de ce “style” de programmation, est de garantir une grande réutilisabilité, et de correspondre au mieux au concept de “Separation of Concerns”.
Architecture Orientée Service
Il est temps de passer au summum de l’Architecture avec ce qu’on appel l’Architecture Orientée Service. Ce type d’Architecture n’est pas à utiliser systématiquement. Mais généralement, si le projet est ambitieux, cette architecture peut offrir d’énormes avantages.
Si on vous demande de développer une application, composés de 2 « modules » bien distincts. PHP pourrait être un langage parfaitement adapté pour le premier, tandis que .NET offrirait de bien meilleurs avantages pour le deuxième. On pourrait prendre comme exemple Python qui propose de nombreuses bibliothèques pour faire du Machine Learning.
L’Architecture Orientée Service consiste à découper votre application, en plusieurs petits « projets », qu’on appel « services » ou « microservices ».
Chaque microservice est responsable d’un aspect de l’application. Une application peut donc être composée de 2, 3, 10, 20, ou d’une infinité de microservices.
L’intérêt ? Chaque microservice peut être implémenté indépendamment des choix technologiques réalisés pour les autres microservices. De même, il devient plus facile de travailler en équipe(s), et de se répartir les différents domaines de l’application.
Cliquez ici pour en savoir plus sur cette architecture.
Les Web Services
L’Architecture est généralement liée à ce qu’on appel des Web Services, c’est à dire un ensemble de fonctionnalités accessibles via le Web (Internet/Intranet). Ces Web Services reposent généralement sur le protocole HTTP qui se décompose simplement de la manière suivante :
- Un verbe (GET, POST, PUT, DELETE, …) pour définir le type d’action souhaitée
- Une URI (sujet de la requête) pour savoir si on parle d’un utilisateur, d’une image, etc…
- Des paramètres (quel utilisateur/image, etc…)
Plus simplement, quand vous consultez une page de recherche Google, une requête est envoyée, contenant :
- Le verbe (qui est “invisible, mais il s’agit du verbe GET pour dire que vous souhaitez récupérer des informations)
- L’URI, qui correspond à l’adresse de l’action pour dire que vous souhaitez contacter la fonctionnalité “recherche”
- Les paramètres de votre recherche
Votre navigateur respecte ce protocole, et vous affiche le code HTML qui vous est envoyé par Google (pour afficher les résultats).
Sauf que le HTML n’est pas toujours le meilleur format, surtout pour communiquer des données exploitables. Dans ce cas, on utilise plutôt XML, ou bien JSON. Ces langages ne sont pas destinés à l’utilisateur, mais plutôt aux applications, pour qu’elle puissent effectuer des traitements sur ces données.
Créer un Web Service, c’est comme créer un site internet, sauf que dans ce cas on ne gère pas l’affichage. Au contraire, on ne retourne que des données brutes (informations de l’utilisateur X par exemple). Ou bien on récupère les paramètres pour effectuer une action précise (modification des informations de l’utilisateur, suppression d’une image, envoi d’un SMS, etc…).
L’interface qui exposera vos données/traitements via HTTP, c’est ce qu’on appel une API (Application Programming Interface). C’est à travers cette API que vous permettrez à d’autres applications d’obtenir des données, ou d’effectuer des traitements.
Comment ça se passe ?
Imaginez que vous disposez maintenant d’un serveur, sur lequel tourne une application “SmsServer”. Cette application comprend un Web Service pour que n’importe quelle autre application puisse lui demander un “service” : envoyer un SMS.
Vous avez une autre application, comme un système de sécurité interne à l’entreprise, qui a besoin d’envoyer des SMS en cas de problème (Serveur en panne, disque dur défaillant, etc…). Cette application a bien évidemment un accès au réseau sur lequel est accessible “SmsServer”. Le code de cette application agit comme un client lorsqu’elle a besoin d’envoyer un SMS. Dans ce cas, le code va exécuter une requête HTTP vers SmsServer qui pourrait être du type “POST http://sms.server.local” (avec en paramètre le SMS à envoyer + le destinataire).
“SmsServer” va récupérer cette requête, effectuer un traitement (envoi du SMS), et retourner le résultat au client (le SMS a bien été envoyé ou non)..
On pourrait imaginer créer un serveur d’envoi d’email aussi, appelé “EmailServer”, et un autre “MessageServer” qui communiquerais à la fois avec “SmsServer” et “EmailServer”.
Tous ces Web Services, développés de la manière la plus unitaire possible, constituent ce qu’on appelle des Micro-Services. Le tout forme un ensemble de modules réutilisables au maximum. Cela permet ainsi de pouvoir déterminer les ressources à allouer, non plus pour une application en générale, mais pour un module en particulier (SmsServer avec haute-disponibilité + haut débit par exemple). Cela présente l’avantage de pouvoir gérer plus précisément les coûts, et de permettre d’avoir un environnement adapté à chaque module (PHP 5 pour SmsServer qui aurait développé il y a longtemps, et la toute dernière version de PHP pour EmailServer par exemple)..
Avantages
- On ne code chaque fonctionnalité qu’une seule fois
- Chaque composant/service peut être remplacé facilement
- Grande maintenabilité
- Fort potentiel d’évolutivité
- Economies sur le long terme
Inconvénients
- Nécessite un Serveur (Apache, Tomcat, Glassfish, Nginx, etc…)
- Coûts sur le court terme
- Plus complexe à mettre en œuvre qu’une architecture monolithique
Advanced Message Queuing Protocol (AMQP)
A ce jour, on utilise beaucoup le protocole AMQP pour faire communiquer les micro-services. Les WebServices peuvent toujours être utilisés afin de proposer une API externe, ou pour pouvoir faire des traitements synchrones (on attend une réponse avant de passer à la suite).
Le protocole AMQP présente de nombreux avantages, mais son utilisation est différente puisque celui-ci se base sur des queues de messages. Les traitements sont donc “asynchrones”.
Ce protocole a plusieurs avantages :
- Pas besoin de Serveur Web
- Possibilité d’envoyer plein de requêtes pour qu’elles soient traitées « plus tard »
Pour que vous compreniez bien l’intérêt, imaginez une application permettant d’envoyer un email. L’utilisateur, depuis l’application Web, déclencherait l’envoi de l’e-mail. Mais ce dernier peut prendre un certain temps à être envoyé (imaginez qu’il faille joindre un PDF, ou que le serveur SMTP ne soit pas disponible au moment de l’envoi). L’intérêt d’AMQP serait de pouvoir répondre à l’utilisateur « L’e-mail sera prochainement envoyé », et de lui permettre de continuer à utiliser l’application, voir même d’éteindre son ordinateur. Le traitement étant asynchrone, une tâche sur le serveur s’occuperait de gérer l’envoi de l’e-mail en temps voulu.
En réalité, le protocole AMQP n’est pas indispensable pour mettre en pratique ce mode de fonctionnement. En effet, il est tout à fait possible d’utiliser un simple fichier, ou encore une base de données MySQL pour stocker les messages, et pour les traiter ensuite. Il existe d’ailleurs d’autres alternatives permettant de répondre au même besoin, telles que Kafka.
Pour résumer
Inversion of Control : Inversion de Contrôle, ce n’est plus l’application qui a le contrôle, mais le framework.
Injection de Dépendances : Un fichier de configuration permettant de définir toutes les dépendances. Le Container injecte des Services dans d’autres Services, et gère leur instanciation.
Programmation Orientée Services : Philosophie poussant à définir la majorité du code dans des services (repose sur l’Injection de Dépendances).
Architecture Orientée Services : Application constituée de plusieurs « microservices », chacun ayant une responsabilité précise, et pouvant potentiellement communiquer avec les autres microservices à travers des protocoles tels que HTTP, ou encore AMQP.