Conception : composants et communication
Motivation et principes
Qu'est-ce que la conception ?
Une fois qu'on a défini ce qu'on veut faire (Analyse), il reste encore à déterminer comment faire (Conception). Il existe en effet une infinité de manière d'implémenter les fonctionnalités définies dans l'analyse. Il faudra donc choisir la plus pertinente au regard des besoins et moyens actuels et futurs.
Une première des choses est de définir la structure de notre projet, i.e. son architecture. Cette étape, très importante, ne doit pas être négligée car impacte fortement la facilité avec laquelle on pourra ensuite écrire le code, le comprendre, le déboguer, l'étendre, le refactorer, le maintenir, etc. Un développeur gagne ainsi en rapidité et confort, rentabilisant rapidement le temps passé sur la conception.
Une mauvaise conception impacte le projet sur le long terme et constitue une dette technique qui sera d'autant plus coûteuse que le projet est avancé. Par exemple, des composants trop interdépendants fera que la modification de l'un impactera les autres, ce qui complexifie toutes modifications. Ainsi, un simple changement peut casser l'intégralité du projet, et nécessiter une réécriture quasi intégrale du code.
Bien qu'il y ai quelques règles à suivre pour faire une bonne conception, il est souvent nécessaire d'avoir une certaine expérience. Certaines erreurs se constatent à l'usage. Comme en mathématiques, il faut parfois changer l'approche, l'angle par lequel on représente le problème. Parfois ajouter ou relâcher des contraintes facilite la conception. Certaines idées toutes bêtes peuvent aussi prendre des jours, voire des années à survenir.
Bons et mauvais principes
Heureusement d'autres personnes se sont plantées bien avant nous, et ont appris de leurs erreurs. Nous n'avons ainsi pas besoin de les répéter, et pouvons bénéficier de leur expérience.
Cette expérience a permit l'établissement de listes de :
- Bons principes : principes de programmation.
- Mauvais principes : code smell/design smell.
- Bons patrons de conceptions : design pattern.
- Mauvais patrons de conceptions : anti-pattern.
Nous verrons certains de ces principes par la suite. Cependant, il est important de ne pas trop se perdre et de garder les choses le plus simple possible (Keep it Simple, Stupide - KISS). Il faut donc avoir une application raisonnée de ces différents principes.
Composants
Il est recommandé de découper son projet en plusieurs composants, ce qui permet de transformer un problème complexe en plusieurs sous problèmes plus simples à résoudre. On appelle cela la Programmation Orientée Composant (POC), à ne pas confondre avec les preuves de concepts (PoC).
Ces composants doivent être les plus indépendants possibles afin de pouvoir les développer, tester, et déboguer, sans avoir à se soucier des autres composants et de leur fonctionnement. Cela permet aussi de substituer un composant par un autre, e.g. pour tester une implémentation différente/nouvelle version. Il est ainsi possible, lorsqu'on développe la version N+1 d'un composant, de conserver la version N afin de pouvoir comparer les implémentations, ou de revenir en arrière en cas de problèmes. Cela, tout en ayant un impact très limité sur les autres composants, i.e. si on "casse" un composant, on ne casse pas les autres, limitant ainsi l'étendue des dégâts. Les modifications au sein d'un composant sont limités au composant lui-même.
Découpages horizontaux
Il est possible de découper son projet "horizontalement", i.e. avec des composants au "même niveau"/côte à côte. Par exemple, un composant pour gérer les données, et un second pour gérer leur affichage.
En effet, l'affichage doit se faire indépendamment de la manière dont sont stockées les données, et le stockage doit se faire indépendamment de la manière dont les données sont affichées. Idéalement, ces deux composants peuvent s'exécuter de manière indépendante. Ainsi, il doit être possible de requêter les données, même si l'affichage est indisponible, e.g. en cours de refonte. De même, pouvoir tester l'affichage sans avoir besoin des données réelles, e.g. en utilisant une source de données différente ou simulée. De plus, si e.g. une requête SQL change, je ne devrais pas avoir à modifier les bouts de code permettant l'affichage de ces données.
Typiquement on sépare l'affichage, les données, et les comportements/règles. Il existe une pléthore de façon d'effectuer un tel découpage, souvent très similaires avec quelques nuances : MVC, MVA, MVP, MVVM, PAC, ADR, modèle de Seeheim, architecture 3-tiers, etc.
Il est aussi possible de découper son projet en fonction de ses différentes fonctionnalités, e.g. gestion des utilisateurs, gestion des produits, executions de tâches planifées, etc. Cela permet ainsi de pouvoir aisément ajouter ou retirer des fonctionnalités sans impacter l'ensemble du projet.
💡 Ces différents découpages ne sont pas exclusifs.
💡 Un composant peut lui-même être redécoupé en sous composants, e.g. l'affichage peut lui-même être redécoupé en structure, mise en forme, interactions.
Découpages verticaux
Une autre manière d'effectuer un découpage de son projet est de le découper "verticalement", i.e. un composant utilisant et dépendant d'un autre, e.g. l'affichage dépendant d'un composant graphique. Il est toujours possible de changer le composant graphique sans impacter l'affichage, mais ce dernier ne peut s'exécuter sans.
Il ne doit pas être nécessaire de savoir le fonctionnement interne d'un composant afin de pouvoir l'utiliser, i.e. le composant est vu comme une boîte noire, c'est le principe d'encapsulation.
💡 Ce principe n'est pas entièrement vrai. Sans avoir besoin de comprendre les détails techniques, il est tout de même nécessaire de comprendre, dans les grandes lignes, le comportement interne de e.g. SQL ou Git, afin de pouvoir correctement les utiliser.
L'encapsulation permet l'abstraction, c'est à dire de pouvoir effectuer des actions en cachant les détails techniques. Par exemple, lorsque vous lisez un fichier avec une fonction , vous n'avez pas à vous soucier de toutes les vérifications et opérations effectuées par le système de fichiers et l'OS. Les bibliothèques graphiques sont un autre exemple d'abstractions.
Un projet peut ainsi avoir plusieurs couches d'abstraction. Les couches les plus basses encapsulent les détails techniques, quand les couches les plus hautes peuvent être une traduction du cahier des charges, en réutilisant la logique et langage métier, permettant potentiellement au client de comprendre le code et modifier certains éléments.
Une manière de faire est d'utiliser l'ECB (Entity-Boundary-Control) pour traduire le cahier des charges. Chaque cas d'utilisation est représenté par une classe implémentant le diagramme d'activité (controller). Les interactions sont représentées par une autre classe pour chaque acteur externe (boundary). Les données sont quant à elles représentées par des classes nommée entity. Le code est ensuite factorisé et restructuré, puis regroupé en un "package de service".
Exemples d'abstractions
💡 Le Design Pattern Façade cache un ensemble de classes et d'opérations complexes derrière une interface unique.
💡 Le Design Pattern Iterator encapsule le parcours d'une structure, permettant de les parcourir indépendamment de leur implémentation (e.g. tableau, liste, ensemble, arbre, etc.) :
Les itérateurs permettent alors d'écrire des algorithmes génériques (e.g. filtres) fonctionnant avec tous types de structures.
Il est aussi possible d'écrire différents types de parcours, e.g. en sens inverse (reverse iterator), excluant certains éléments.
Les itérateurs sont notamment utilisé en interne par :
Il est aussi possible d'écrire différents types de parcours, e.g. en sens inverse (reverse iterator), excluant certains éléments.
Les itérateurs sont notamment utilisé en interne par :
💡 Les DAO (Data Access Object) sont une abstraction encapsulant l'accès à des données persistantes. On manipule alors les données via des classes (e.g. , , etc.) qui se chargent d'effectuer les opérations demandées via, e.g., des requêtes SQL, des lectures/écritures dans des fichiers, des commandes Shell, etc.
Découpage "éclaté"
Une troisième manière de découper son projet, est de le découper en fonctionnalités qui seront éclatées dans différents "services".
L'architecture générale est alors découpée en composants qu'on peut (plus ou moins abusivement) appeler services (e.g. GUI, API REST, Database, etc.). Chaque fonctionnalité est un "lot" (package) de composants (e.g. sous la forme d'un dossier) qui seront déployés dans les différents services, e.g. une fonctionnalité de gestion d'utilisateurs constituée de composants qui étendent la GUI, l'API REST, la base de donnée, etc.
UML
Il est possible de représenter les différents composants via un diagramme de déploiement, décrivant aussi les matériels sur lesquels les composants seront déployés.
💡 Il est possible de préciser les caractéristiques (mémoire, processeur, etc.) des matériels.
💡 Il est aussi possible d'utiliser des diagrammes de composants ou des diagrammes de package. En pratique, le diagramme de déploiement suffit.
Communication
Les différents composants vont communiquer entre eux, et ainsi s'envoyer des messages qui peuvent prendre différentes formes (e.g. appels de fonctions, requête REST, etc.). Pour que cela fonctionne, il faut que le composant qui reçoit le message puisse le comprendre. Il est ainsi nécessaire de spécifier un protocole de communication que le composant s'engage à implémenter et à respecter, un peu comme un contrat. Cela peut prendre la forme d'une API REST, d'une classe exposant des méthodes, etc.
Le composant doit être une boîte noire pour l'extérieur qui ne doit pas accéder à ses éléments internes (cf principe d'encapsulation). Ainsi, les interactions avec le composant ne doit se faire qu'au travers d'une interface publique, e.g. par le biais d'un DP façade.
Les messages
Architecture monolithique
Dans une architecture monolithique les composants s'exécutent au sein du même exécutable. Les messages sont usuellement implémentés sous la forme d'appels de fonctions. Pour éviter certains problèmes (e.g. boucles infinies), on va souvent distinguer l'enclenchement de l'action et son traitement :
- : j'enclenche l'action.
- : je traites/réagis à l'action.
Les appels de fonctions peuvent être synchrone (j'attends la réponse) ou asynchrone (je continue l'exécution sans attendre la réponse). Les appels asynchrones peuvent être implémentés par :
- un callback : fonction passée en paramètre et appelée lorsque l'action est finie (e.g. ).
- une fonction asynchrone : dépend du langage de programmation utilisé (e.g. )..
Service Oriented Architecture (SOA)
Dans une architecture SOA,les composants s'exécutent dans plusieurs exécutables fournissant des services différents. Il n'est ainsi pas possible d'utiliser les appels de fonctions pour transmettre des messages. On utilise alors des techniques de communications inter-processus (IPC) :
- Fichiers FIFO : pipes, sockets.
- Événements : message queue, signaux unix.
- Données : sémaphores, mémoire partagée.
Le message doit alors être converti en données binaires afin de pouvoir être transmis (e.g. JSON). Pour transmettre un objet, il faudra alors le sérialiser (i.e. le transformer en données binaires), puis le désérialiser à la réception (i.e. le récréer à partir des données binaires).
Dans une architecture de micro-services, les composants se trouvent sur des serveurs différents. Les sockets sont alors utilisées avec un protocole réseau adapté aux besoins du composant. Par défaut, on utilisera généralement une API REST.
⚠ Le coût des communications dans une architecture de micro-service est très important. En effet, envoyer une requête réseau est bien plus coûteux qu'un simple appel de fonction.
Event-Driven Architecture (EDA)
Ces différentes méthodes de communications peuvent être abstraites par un système d'événements. Le principe est assez simple : une source d'événement diffuse (broadcast) des événements, et les fonctions abonnées (subscribed) à cet événement sont appelées. Il est possible d'implémenter ce système de deux manières différentes :
- DP Observer : la source stocke et appelle elle-même les fonctions abonnées.
- DP Reactor : une boucle d'événements appelle les fonctions abonnées (e.g. JS, systèmes de serveurs, etc.).
💡 En fonction du contexte, la fonction abonnée peut être appelée listener ou handler.
Gestion des messages
- Interceptor DP
- Adapter
- Proxy : log, cache, etc.
- Mediator DP
- Broker : load balancing, redirect, etc.
- DP Commande : e.g. associer GUI btn à une action controler.
- Data Transfert Object : regrouper plusieurs requêtes avant d'envoyer
- Message queuing
- Chaîne de responsabilité : pour les handler, un seul s'en occupe.
UML
Diagramme de séquence
Le diagramme de séquence permet de montrer, dans un ordre chronologique, les interactions entre divers entités dans le cadre d'un scénario donné. Ils sont notamment très utilisés pour décrire des protocoles réseaux.
Diagramme de Communication
Lorsqu'il y a trop d'entités, et peut de messages entre deux entités, il est possible d'utiliser un diagramme de communication pour plus de lisibilité :