L'Architecture hexagonale : Introduction

Découvrez l'architecture hexagonale : principes, exemples, intégration avec Spring Boot. Optimisez votre code pour des applications mobiles robustes.

Découvrons aujourd’hui l'univers captivant de l'architecture hexagonale. Cette approche du développement d'applications mobiles repose sur des principes fondamentaux, offrant une structure robuste et évolutive. Dans cet article, explorez les bases de l'architecture hexagonale, découvrez des exemples concrets sur GitHub et apprenez à l'intégrer avec Spring Boot. Optimisez votre code, maîtrisez l'inversion des dépendances et transformez votre façon de concevoir des applications mobiles.

Bienvenue dans le futur du développement logiciel !

Qu'est-ce que l'architecture hexagonale ?

L'architecture hexagonale redéfinit la conception des applications mobiles. À la base de cette approche novatrice se trouvent des principes clés, sculptant une structure en forme d'hexagone. Cette méthode se distingue par son agilité, son adaptabilité et son aptitude à créer des applications robustes. Découvrons les fondements de l'architecture hexagonale pour comprendre comment elle redéfinit le développement logiciel.

L'architecture hexagonale transcende les schémas conventionnels de développement logiciel. Imaginons-la comme une vue aérienne de votre application, où un hexagone représente le cœur, le noyau de votre système. Plongeons dans les détails de cette approche novatrice.

Au centre de cette structure, nous trouvons l'hexagone central. C'est le cœur, le noyau où réside la logique métier de votre application. Cet espace défini par l'hexagone représente l'essence même de ce que votre application offre à ses utilisateurs.

Les côtés de l'hexagone représentent les couches périphériques. Chacune de ces couches a un rôle spécifique dans l'interaction de l’application avec le monde extérieur. De la gestion des entrées/sorties à la persistance des données, ces couches entourent le noyau central, mais sans créer de dépendances directes avec lui.

Les interactions entre le cœur et les couches périphériques se font à travers des ports et adaptateurs. Les ports définissent des interfaces au sein du noyau, tandis que les adaptateurs fournissent des implémentations concrètes pour ces interfaces. Cette modularité offre une flexibilité essentielle, permettant à l'application de s'adapter sans altérer sa logique métier.

Enfin, l'inversion des dépendances est le principe qui gouverne l'architecture hexagonale. Plutôt que d'avoir des dépendances directes, le cœur de l'application dépend d'abstractions définies par les ports. Cette inversion crée un environnement flexible permettant des modifications sans impacter la stabilité du système.

Pour comprendre pleinement l'architecture hexagonale, explorons des exemples concrets. 

L'architecture hexagonale ne se contente pas de suivre les sentiers battus du développement logiciel. Imaginez-la comme une vue aérienne de votre application, où un hexagone représente le cœur, le noyau vibrant de votre système. Plongeons dans les détails pour rendre cette approche plus tangible.

Au cœur, l'hexagone central incarne la logique métier de votre application. C'est là que réside l'essence de ce que votre application offre à ses utilisateurs. Autour de ce noyau, les côtés de l'hexagone représentent les couches périphériques. De la gestion des entrées/sorties à la persistance des données, chaque couche a un rôle précis dans l'interaction de l'application avec le monde extérieur.

Exemples concrets

Imaginons une application de gestion de tâches où le cœur de l'hexagone représente la logique de gestion des tâches, des deadlines, etc. Les côtés de l'hexagone pourraient inclure une couche d'interface utilisateur, une couche de persistance des données, et une couche de services externes.

Les interactions entre le cœur et les couches périphériques s'effectuent à travers des ports et adaptateurs. Les ports définissent des interfaces au sein du noyau, tandis que les adaptateurs fournissent des implémentations concrètes pour ces interfaces. Cette modularité offre une flexibilité essentielle, permettant à l'application de s'adapter sans compromettre sa logique métier.

Supposons que le port "GestionTâchesPort" définisse les opérations nécessaires à la gestion des tâches. L'adaptateur "GestionTâchesAdapter" fournirait l'implémentation concrète de ces opérations, interagissant avec la base de données et les services externes.

Enfin, l'inversion des dépendances règne en maître dans l'architecture hexagonale. Plutôt que des dépendances directes, le cœur dépend d'abstractions définies par les ports. Cette inversion crée un environnement souple, permettant des modifications sans secouer la stabilité du système.

Plutôt que d'avoir des dépendances directes vers la base de données, le cœur dépendrait d'interfaces définies dans le port "PersistancePort", laissant les détails d'implémentation à l'adaptateur "PersistanceAdapter".

En résumé, l'architecture hexagonale offre une vision stratégique du développement logiciel. Elle place la logique métier au centre, entourée de couches flexibles facilitant l'interaction avec le monde extérieur. Cette approche, avec son hexagone central et ses principes fondamentaux, ouvre la voie à des applications mobiles robustes, adaptables et pérennes.

Les principes de l'architecture hexagonale

L'architecture hexagonale repose sur des fondements solides, formant une structure en forme d'hexagone pour créer des applications mobiles robustes. Décortiquons les principes clés qui définissent cette approche innovante :

  • L'architecture hexagonale est structurée en couches fondamentales. De la couche d'infrastructure à celle de persistance, chaque strate joue un rôle crucial. Cela offre une organisation claire, favorisant la stabilité et la modularité.
  • Le point d'entrée représente l'accès initial à l'application, tandis que la logique métier dicte son fonctionnement interne. Cette dualité assure une expérience utilisateur fluide, équilibrant l'interaction extérieure avec la logique interne.
  • Fondamentale à l'architecture hexagonale, l'inversion des dépendances renverse les schémas traditionnels. Cette approche permet à l'application de s'adapter aux changements sans compromettre sa stabilité. Elle crée un environnement où la logique métier guide les détails d'implémentation.

Mise en pratique de l'architecture hexagonale

Découvrons comment concrètement mettre en œuvre l'architecture hexagonale dans le développement d'applications mobiles. Plongeons dans des exemples tangibles pour comprendre son impact réel.

Explorez un exemple concret sur GitHub où le code source d'une application est dévoilé. Chaque composant, de la couche d'infrastructure à la couche de persistance, est clairement défini. Visualisez comment ces éléments s'entremêlent pour former une structure cohérente. Cette transparence simplifie le processus de développement, permettant une compréhension facile et une évolution efficace de l'application.

L'intégration pratique de l'architecture hexagonale est facilitée avec Spring Boot. Cette union offre une approche concrète pour développer des applications mobiles robustes. Elle simplifie la gestion des dépendances et maintient la flexibilité, permettant aux développeurs de se concentrer sur la création plutôt que sur des considérations techniques. 

L'architecture hexagonale dans le contexte du Domain-Driven Design

Plongeons dans la synergie puissante entre l'architecture hexagonale et les principes du Domain-Driven Design (DDD), développant ainsi des applications mobiles de plus grande qualité.

L'architecture hexagonale et le Domain-Driven Design (DDD) fusionnent harmonieusement pour définir des modèles de domaine robustes offrant ainsi une approche complète du développement logiciel.

Lorsque nous plongeons l'architecture hexagonale dans le contexte du Domain-Driven Design (DDD), une collaboration symbiotique émerge. Ces deux approches, axées sur la compréhension profonde du domaine métier, se renforcent mutuellement.

1. Collaboration Harmonieuse

Imaginons une application de commerce électronique. Dans l'architecture hexagonale, le cœur de l'hexagone représente la gestion des commandes, des transactions, et des stocks, constituant la logique métier centrale. Dans le contexte du DDD, ces entités deviennent les agrégats du domaine, chacun avec son propre cycle de vie et ses règles métier spécifiques. Ainsi, l'hexagone central et les agrégats du DDD collaborent harmonieusement pour façonner le modèle de domaine.

2. Réflexion sur le Contexte Limité

Poursuivons avec la réflexion sur le contexte limité du DDD. Supposons que notre application de commerce électronique ait également un module de gestion des utilisateurs. Dans l'architecture hexagonale, cela devient une autre zone centrale, avec ses propres ports et adaptateurs. Dans le contexte limité du DDD, ce module représente son propre sous-domaine avec ses règles métier distinctes. Cette approche permet une séparation claire des préoccupations et une meilleure compréhension du modèle de domaine, alignant ainsi l'architecture hexagonale avec les principes du DDD.

3. Alistair Cockburn et les Fondements

Alistair Cockburn, un pionnier du Domain-Driven Design, souligne l'importance de définir des interactions précises entre les entités du domaine. Dans l'architecture hexagonale, cela se traduit par la définition précise des ports et adaptateurs, offrant une interface bien définie pour chaque interaction. Cette synchronicité entre les principes de Cockburn et l'architecture hexagonale garantit une compréhension approfondie du domaine et une mise en œuvre logicielle qui reflète fidèlement la réalité métier.

L'architecture hexagonale et le Domain-Driven Design forment une alliance puissante. En utilisant des exemples concrets, nous avons vu comment ces approches complémentaires collaborent pour créer des modèles de domaine clairs, des contextes limités bien définis, et des applications mobiles riches en fonctionnalités métier.

Nos conseils pratiques et astuces

Explorez des conseils concrets et des astuces judicieuses pour optimiser l'utilisation de l'architecture hexagonale dans le développement d'applications mobiles.

Dans la mise en œuvre de l'architecture hexagonale, privilégiez la clarté. Des noms de classes explicites aux commentaires informatifs, assurez-vous que chaque composant de votre application est compréhensible. La transparence facilite la collaboration et la maintenance à long terme.

Investissez dans des tests unitaires approfondis. L'architecture hexagonale favorise la testabilité, alors profitez-en. Des tests solides garantissent la stabilité de votre application et facilitent l'identification rapide des problèmes potentiels.

Accompagnez votre code d'une documentation complète. Décrivez les choix architecturaux, les interactions clés, et les modèles de domaine. Une documentation détaillée facilite l'intégration de nouveaux membres dans l'équipe et assure une compréhension globale du projet.

Soyez sélectif quant aux dépendances. Limitez-vous aux dépendances nécessaires pour éviter la complexité inutile. Une architecture hexagonale bien conçue privilégie la simplicité, ce qui facilite la maintenance et l'évolutivité.

Adoptez une approche itérative. L'itération continue associée au réajustement est essentielle. Recueillez les retours, identifiez les améliorations possibles, et évoluez constamment. Cette approche flexible s'aligne parfaitement avec les principes de l'architecture hexagonale.

L'architecture hexagonale se révèle comme une approche incontournable pour le développement d'applications mobiles. Avec ses principes solides, sa mise en pratique transparente, et sa synergie avec le Domain-Driven Design, elle offre une solution robuste et flexible.

Priorisez la clarté, investissez dans des tests unitaires approfondis, documentez judicieusement, évitez les dépendances superflues, et adoptez une approche itérative pour un succès continu.

En embrassant l'architecture hexagonale, vous développez des applications mobiles plus résilientes et créez une base pour l'innovation future. Restez agile, apprenez constamment, et évoluez avec votre application.

L'architecture hexagone est le socle sur lequel repose l'avenir du développement logiciel mobile.

Sommaire
Nos autres catégories
Partager sur :
Nos autres catégories
Partager sur :

Ne manquez rien
Abonnez-vous à notre newsletter

Notre newsletter tous les mois :
Je m'abonne
Merci ! C'est dans la boîte :)
Oops! Something went wrong while submitting the form.

Nos experts vous parlent
Le décodeur

L’Architecture Hexagonale sur un projet Web + Mobile (Partie 1 sur 5)
27/2/2024

Hola, je vais vous présenter le début d'une nouvelle série d'articles dédiées à la construction d'un projet Web et Mobile en mettant à profit l'Architecture Hexagonale. Durant toute cette série, nous allons explorer comment la logique métier peut être partagée et gérée efficacement à travers différentes plateformes.

Nos objectifs

Nous allons avoir plusieurs objectifs à atteindre au fil de ce projet :

  • Apprendre comment développer une application Web et Mobile à la fois en mettant à profit les technologies modernes (Nx, Expo, Remix, Vitest, etc.)
  • Comprendre les principes de l'Architecture Hexagonale et comment l'appliquer pour optimiser le partage de la logique métier
  • Gagner en compétence et en confiance pour lancer votre propre projet multi-plateforme, tout en développant une base de code propre et maintenable

La structure de la série

Comme je l'ai dis au début de ce premier article, ce projet donnera lieu à une série d'articles qui sera structurée de cette manière :

  • Partie 1 (cet article) : présentation du projet et mise en place d'un monorepo avec Nx
  • Partie 2 : développer sans UI avec l'Architecture Hexagonale
  • Partie 3 : partager de la logique métier et des composants entre le Web et le Mobile
  • Partie 4 : refactor serein avec les tests et l'Architecture Hexagonale
  • Partie 5 : déploiement Web et Mobile avec Netlify et EAS

Le projet

Le projet qui va nous aider à mettre en avant l'Architecture Hexagonale est un outil de gestion de budget que l'on appellera broney (le bro qui t'aides à gérer ta money 😎). Cet outil sera composé de deux applications, une première, web, développée avec Remix et une deuxième, mobile, développée avec Expo. Nous aurons donc 2 applications React et React Native avec un package TypeScript qui contiendra la logique métier partagée entre ces 2 applications.

Stack

La Stack que j'ai choisi est très subjective, on y trouve quelques frameworks qui mérite selon moi plus de lumière (Remix notamment et Nx face à NextJS et Turborepo). Néanmoins il est important de comprendre que peu importe les frameworks et libraires utilisées, le coeur de l'application sera complètement agnostique et réutilisable dans n'importe quel contexte.

Fonctionnalités

Pour mettre en avant l'Architecture Hexagonale nous allons avoir besoin de développer quelques fonctionnalités pour avoir de la logique métier. Nous allons nous focus sur les fonctionnalités suivantes :

  • Mettre en place le storage : react native mmkv pour le mobile et localStorage pour le web
  • Gérer les catégories : lister, ajouter, modifier et supprimer
  • Gérer les portefeuilles : lister, ajouter, modifier et supprimer
  • Gérer les transactions d'un compte : lister, ajouter, modifier et supprimer
  • Authentification avec Supabase
  • Dynamiser toute l'app avec Supabase

Modèle de données

Pour mettre en place les fonctionnalités nous allons avoir besoin des entités suivantes :

  • Wallet, un portefeuille qui a un solde négatif ou positif (par exemple on peut avoir le portefeuille "Compte Principal Julien" qui a un solde positif de 1000€)
  • Category, des catégories servant à préciser le contexte des transactions faites (par exemple on a les catégories "Maison", "Restaurants" et "Divertissements")
  • Transaction, les transactions sont liées à un portefeuille et à une catégorie pour savoir où l'argent est transférée (par exemple on a une transaction du portefeuille "Compte Principal Julien" de 50€ sur la catégorie "Restaurants")

Mise en place du monorepo avec NX

Initialisation du projet

Nous allons utiliser les commandes de Nx pour initialiser notre projet.

➜ npx create-nx-workspace@latest

✔ Where would you like to create your workspace? · broney
✔ Which stack do you want to use? · none
✔ Package-based monorepo, integrated monorepo, or standalone project? · package-based
✔ Enable distributed caching to make your CI faster · Yes

Avec cette commande nous avons le projet Nx configuré de base et sans libs pour le moment. Nous allons travailler avec le style Package-Based Repos qui nous offre plus de liberté en limitant le couplage avec Nx si jamais on souhaite changer facilement d'outil de monorepo. Cela permet également d'avoir des node_modules différents pour chaque app ou lib du projet. En savoir plus sur les différents style d'implémentation de Nx.

Création de la lib core

Nous allons maintenant ajouter notre première lib, la plus importante : core. En effet, c'est dans cette lib que nous allons mettre notre Architecture Hexagonale et la logique métier qui sera utilisée par nos applications Web et Mobile.

➜ nx g @nx/js:lib libs/core

✔ Which unit test runner would you like to use? · vitest
✔ Which bundler would you like to use to build the library? Choose 'none' to skip build setup. · rollup

Cette commande nous a généré une lib avec le framework de test Vitest, une config eslint et prettier que l'on peut adapter à nos preferences que je ne détaillerai pas ici.

Il est possible de compiler notre lib avec la commande nx core build et d'executer les tests avec nx core test.

Mise en place de la CI

Maintenant que nous avons les tests setup ainsi que prettier et eslint, il est pertinent de mettre en place une CI pour avoir du feedback régulier sur la bonne tenue du code. Pour la CI nous allons simplement suivre la documentation de Nx et utiliser les GitHub Actions.

Nous allons donc simplement ajouter un fichier .github/workflows/ci.yml assez simple qui peut être étoffé.

name: CI
on:
	push:
	branches:
	- main
	pull_request:

jobs:
	main:
		runs-on: ubuntu-latest
		steps:
			- uses: actions/checkout@v4
				with:
					fetch-depth: 0
			# Cache node_modules
			- uses: actions/setup-node@v3
				with:
					node-version: 20
					cache: 'yarn'
			- run: yarn --no-progress --frozen-lockfile
			- uses: nrwl/nx-set-shas@v4.0.4

			- run: npx nx format:check
			- run: npx nx affected -t lint,test,build --parallel=3

Cette simple CI permet vérifier le bon formatage prettier, d'effectuer les validations eslint et de build et de s'assurer que les tests sont sans erreurs.

Structure du projet

Rentrons plus en détails dans ce que l'on vise comme structure de projet une fois les apps mise en place et notre lib core développée avec l'architecture hexagonale.

- apps

    - mobile : notre application React Native développée avec Expo
    - web
: notre application React développée avec React

- libs
    - ui
: nos composants React et React Native utilisés par les apps web et mobile
    - tailwind
: notre configuration tailwind utilisée par les apps web et mobile ainsi que la lib ui
    - core
: notre architecture hexagonale qui contient le coeur de notre application et toute la logique métier réutilisable par les apps web et mobile

Pour aller plus loin, on peut très bien envisager d'avoir une app en plus pour un Storybook.

La lib qui va nous intéresser et la lib core évidemment. Elle sera structurée de cette manière :

- libs
    - core
         - src
              - wallet
                   - tests
                        - wallet.service.test.ts
: la logique métier testées
                        - wallet.test.ts
: les règles métiers testées
                   - domain
                        - wallet.ts
: l'entité qui représente les portefeuilles et qui contient des règles métiers
                        - wallet.repository.ts
: le contrat qui détermine comment manipuler l'entité pour lister, ajouter, etc.
                        - wallet.service.ts : le service qui consume une implémentation de contrat

                   - infrastructure
                        - in-memory-wallet.repository.ts
: une implémentation du contrat
                        - local-storage-wallet.repository.ts
: idem
                        - supabase-wallet.repository.ts : idem
                   - user-interface
                        - wallet.store.ts :
un store zustand vanilla, utilisable dans n'importe quel environnement javascript et qui sera utilisé dans nos apps
              - category
                   - ...
           - ...

Nous verrons le contenu de chaque fichiers ainsi que les détails du fonctionnement de ces derniers dans le prochain article !

Conclusion

Nous avons terminé le premier article de cette série sur le développement d'une application web et mobile avec l'Architecture Hexagonale et le partage de la logique métier et des composants UI.

Dans cette première partie nous avons vu comment mettre un place un monorepo et nous avons pourquoi et comment ce monorepo va nous aider à partager la logique métier entre nos différentes applications. Nous avons également bien délimité le périmètre et les fonctionnalités attendues pour notre première version, le MVP, de broney.

Enfin, à la fin de cet article nous avons commencé à entrevoir la structure du projet en mettant en évidence l'Architecture Hexagonale, ce sera le thème de la deuxième partie : Développer sans UI avec l'Architecture Hexagonale.

Le multi-tenant, un indispensable pour une solution SaaS
23/12/2023

Lorsque l’on développe une solution SaaS, il est nécessaire de bien penser son architecture, surtout si à l’avenir vous réfléchissez déjà à faire découpler plusieurs instances de celle-ci.

Pour imager, prenons pour exemple un site e-commerce.

Vous pouvez faire le choix de partir sur une architecture simple pour votre MVP, avec tout simplement votre boutique à Paris, mais dès lors où le besoin d’avoir plusieurs boutiques se présente, plusieurs questions vont venir à vous.

Ces questions pourraient concerner :

La gestion du stock : est-elle centralisée ? Y-a-t’il un stock par boutique ?

La gestion des produits : est-ce que chaque boutique est indépendante, est-ce qu’elle a ses propres produits ?

La gestion des utilisateurs : est-ce que je stocke les données utilisateurs par boutique ? Est-ce que j’ai une base commune d’utilisateurs ?

Toutes vos réponses vont impacter la façon dont vous allez mettre en place le multi-tenant.

Le multi-tenant

Vous l’avez compris, on parle de multi-tenant lorsque l’on doit gérer plusieurs contextes dans une application, si l’on devait reprendre notre exemple précédent on considèrerait chaque boutique comme un contexte.

Architecture single-tenant vs multi-tenant

Gestion en single-database

La gestion du multi-tenant au moyen d'une seule base de données présente plusieurs avantages significatifs.

Architecture multi-tenant single-database

Tout d'abord, elle simplifie considérablement la maintenance car il n'y a qu'une seule base a gérer en cas de bugs ou de restauration des backups.

De plus, la base de données demeure relativement simple à gérer avec l'utilisation d'un champ tenant_id (store_id) pour distinguer les différents tenants.

Cela offre un avantage financier car il n'y a pas de surcoût au niveau de l'infrastructure.

L'approche du multi-tenant avec une seule base de données comporte également certains inconvénients notables.

Dans le cas de l'utilisation d'un Framework PHP tel que Laravel ou Symfony par exemple, l'adaptation des packages de la communauté ainsi que des requêtes SQL est nécessaire, ce qui peut entraîner des coûts de développements supplémentaires.

En effet, il faudra ajouter un critère à chaque requête pour spécifier le bon tenant à utiliser, un oubli entraînerait des conséquences assez importantes.

De plus, la centralisation des données peut rendre la restauration de données complexe si on a besoin de restaurer les données pour un tenant précis.

Gestion en single-database multi-schema

Une alternative possible dans l'implémentation du multi-tenant consiste à attribuer à chaque tenant ses propres tables au sein de la base de données.

Architecture multi-tenant multi-schema

Cette approche offre une isolation accrue et la gestion des données s'en retrouve simplifiée. Tout comme pour l'implémentation précédente, la restauration des données reste simple. En adoptant cette approche, on ajoute donc une séparation des données de tenants.

Cependant, cette approche présente également quelques inconvénients.

La nécessité de restaurer un tenant spécifique peut être plus compliquée, car il faut sélectionner individuellement chacune des tables lors du backup ou lors de la restauration.

De plus, à mesure que le nombre de tenants augmente, le nombre de tables associées peut devenir considérable, ce qui risque de compliquer la gestion à long terme.

Si des modifications sont apportées à la structure d'une table, chaque table dupliquée pour chaque tenant doit être mise à jour individuellement.

Cela rend également la gestion des migrations compliquées avec des frameworks comme Laravel ou Symfony puisqu'ils n'ont pas été prévu à cet effet.

Gestion en multi-database

L'utilisation du multi-tenant avec une base de données spécifique par tenant offre plusieurs avantages.

Architecture multi-tenant multi-database

Une simplicité côté développement, où il suffit de spécifier quel tenant est utilisé sans adaptations complexes de packages ou de requêtes SQL. L'implémentation est donc plus rapide et le code plus facile à maintenir.

Pour le backup et la restauration, il suffit de le faire sur la base de données du tenant.

On peut optimiser les performances en ajustant les ressources allouées à chaque tenant en fonction de ses besoins.

C’est également le schéma idéal si dans un projet chaque tenant correspond à un site client et que ces clients souhaitent une confidentialité et isolation de leur données.

Et pour les désavantages de cette implémentation, on peut avoir plus de serveurs ou plus de base de données à maintenir, il faut avoir quelques bases côté infrastructure pour mettre en place et configurer les environnements et le coût d'infrastructure sera plus conséquent.

Conclusion

Chaque architecture a ses avantages et inconvénients, la décision devra se prendre en fonction de vos besoins, de vos coûts, de l’effectif de votre équipe et de nombreux facteurs qui composeront la pérennité de votre projet.

Sur le Framework Laravel, en PHP, plusieurs packages existent pour gérer le multi-tenant. Si on devait en opposer deux, le package Laravel Multitenancy de Spatie propose une implémentation simple et légère qu’il faudra agrémenter de “Tasks” selon le mode de gestion que vous allez choisir, tandis que le package Tenancy d’ArchtechX propose plutôt une architecture plus complexe qui répond à un maximum de besoins avec plus d'opinion.

Il est primordial de s’intéresser à chacune des solutions existantes et de créer des POCs avant de se lancer tête baissée dans l’implémentation du multi-tenant.

Et vous ? Lequel de ces packages choisiriez-vous ?

Si vous hésitez encore pas de panique ! Nous étudierons sans doute plus en détails les différences dans un prochain article.

L’Architecture Hexagonale sur un projet Web + Mobile (Partie 2 sur 5)
28/2/2024

Dans l'article précédent nous avons initialisé notre monorepo, la CI, le framework de test et préparé la structure de notre projet et plus précisément de notre Architecture Hexagonale pour la lib core.

Dans ce nouvel article de la série notre objectif va être de mettre en place l'Architecture Hexagonale et de montrer comment grâce à elle nous allons pouvoir développer et créer de la logique métier sans UI (donc sans ouvrir le navigateur ou l'app mobile). Pour cela nous allons travailler en TDD (Test-Driven Development, vous pouvez voir mon article à ce sujet) et utiliser le feedback des tests.

L'Architecture Hexagonale

La structure cible

Pour rappel, voici la structure que l'on va mettre en place à l'issue de cet article :

- src
    - wallet
       - __ tests __
         - wallet.service.test.ts
       - domain
         - wallet.ts
         - wallet.repository.ts
         - wallet.service.ts
       - infrastructure
         - in-memory-wallet.repository.ts
         - local-storage-wallet.repository.ts
         - mmkv-wallet.repository.ts
       - user-interface
         - wallet.store.ts

Chose promise, chose due ! Nous allons maintenant rentrer dans le détail de chaque fichier, à quoi ils servent et ce qu'ils contient.

Développer en TDD

Lorsqu'on travaille en TDD on commence par le test et ce test va nous guider vers un objectif. Il va nous assurer qu'on suit le bon chemin à l'aide de la boucle de feedback régulière qu'on obtient à l'aide des tests. Pour en savoir plus sur la méthodologie à suivre pour faire du TDD je vous invite à nouveau à lire mon article à ce sujet.

Nous allons commencer par travailler sur l'entité Wallet qui correspond à un portefeuille qui a un solde négatif ou positif (par exemple on peut avoir le portefeuille "Compte Principal Julien" qui a un solde positif de 1000€).

Voici les tests mis en place pour cette entité :

describe('Wallet Service', () => {
	let service: WalletService

	beforeEach(() => {
		const repository = new InMemoryWalletRepository()
		service = new WalletService(repository)
	})

	test('getAll > should retrieve all wallets', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }

		await service.create(newWallet)
		const retrievedWallets = await service.getAll()

		expect(retrievedWallets).toEqual([newWallet])
	})

	test('get > should retrieve a wallet according to an id', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }

		await service.create(newWallet)
		const retrievedWallet = await service.get(newWallet.id)

		expect(retrievedWallet).toEqual(newWallet)
	})

	test('create > shoudl create a wallet', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }

		const createdWallet = await service.create(newWallet)
		const retrievedWallets = await service.getAll()
		const retrievedWallet = await service.get(createdWallet.id)

		expect(createdWallet).toEqual(newWallet)
		expect(retrievedWallets).toEqual([newWallet])
		expect(retrievedWallet).toEqual(newWallet)
	})

	test('update > should update the specified wallet', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }
		const updatedWallet = { id: '1', name: 'Wallet 1', balance: 100 }

		await service.create(newWallet)
		const retrievedWallet = await service.get(newWallet.id)
		const modifiedWallet = await service.update(updatedWallet)
		const retrievedModifiedWallet = await service.get(modifiedWallet.id)

		expect(retrievedWallet).toEqual(newWallet)
		expect(modifiedWallet).toEqual(updatedWallet)
		expect(retrievedModifiedWallet).toEqual(updatedWallet)
	})

	test('delete > should delete a wallet according to an id', async () => {
		const newWallet = { id: '1', name: 'Wallet 1', balance: 0 }

		await service.create(newWallet)
		const retrievedWallet = await service.get(newWallet.id)
		await service.delete(newWallet.id)
		const retrievedWallets = await service.getAll()

		expect(retrievedWallet).toEqual(newWallet)
		expect(retrievedWallets).toEqual([])
	})
})

On peut comprendre via ces tests que les cas d'utilisations de notre entité sont :

  • getAll, récupération de tous les portefeuilles
  • get, récupération d'un portefeuille en particulier
  • create, création d'un portefeuille
  • update, mise à jour d'un portefeuille
  • delete, suppression d'un portefeuille

Nous allons voir maintenant comme réussir à mettre en place ces tests.

Domain

Nous allons commencé par créer le contenu de la partie Domain. Dans cette partie nous allons retrouver tout ce qui représente le problème à résoudre (problème métier). C'est une partie qui doit être totalement indépendante.

L'entité

Commençons par créer notre entité Wallet correspondant à un portefeuille.

type Wallet = {
	// un identifiant unique (ex: 4d0c2e72-be1a-4e2c-a189-2f321fcdc3a4)
	id: string

	// un nom (ex: Compte Principal Julien)
	name: string

	// un nombre positif ou négatif pour le solde (ex: +1000€)
	balance: number
}


Le repository

Maintenant que notre entité est définie, nous allons définir une interface que l'on appelle également port qui va préciser comment interagir avec cette entité. Nous utilisons ici un modèle de conception d'inversion de dépendances qui nous permet de rester totalement libre sur les outils à utiliser pour respecter cette interface. Nous pourrons très bien implémenté cette interface en utilisant une base de données, une API ou un localStorage par exemple, le domaine s'en fiche.

interface WalletRepository {
	getAll(): Promise
	get(walletId: string): Promise
	create(wallet: Wallet): Promise
	update(wallet: Wallet): Promise
	delete(walletId: string): Promise
}

Le service

Nous avons notre entité et nous savons commencer interagir avec, maintenant nous allons créer un service qui va consumer une implémentation du de notre interface repository (partie suivante dans l'infrastructure).

class WalletService implements WalletRepository {
	constructor(private repository: WalletRepository) {}

	getAll() {
		return this.repository.getAll()
	}

	get(walletId: string) {
		return this.repository.get(walletId)
	}

	create(wallet: Wallet) {
		return this.repository.create(wallet)
	}

	update(wallet: Wallet) {
		return this.repository.update(wallet)
	}

	delete(walletId: string) {
		return this.repository.delete(walletId)
	}
}

Infrastructure

L'infrastructure est composée des différentes implémentations des ports du domaine, on les appelle également Adapters. Ici, nous aurons du code spécifique pour consommer une technologie concrète (une base de données, une API, etc.). C'est une partie qui ne doit dépendre uniquement du domaine.

L'implémentation du repository

Nous allons maintenant voir l'une des implémentation possible de notre WalletRepository. Pour commencer nous allons faire du in-memory, pratique notamment pour la mise en place des premiers tests de nos cas d'utilisations.

class InMemoryWalletRepository implements WalletRepository {
	private wallets: Wallet[] = []

	getAll() {
		return Promise.resolve(this.wallets)
	}

	get(walletId: string) {
		return Promise.resolve(this.wallets.find((wallet) => wallet.id === walletId))
	}

	create(wallet: Wallet) {
		this.wallets.push(wallet)
		return Promise.resolve(wallet)
	}

	update(wallet: Wallet) {
		const index = this.wallets.findIndex((w) => w.id === wallet.id)
		this.wallets[index] = wallet
		return Promise.resolve(wallet)
	}

	delete(walletId: string) {
		const index = this.wallets.findIndex((w) => w.id === walletId)
		this.wallets.splice(index, 1)
		return Promise.resolve()
	}
}

Comment dis précédemment, il s'agit d'une des multiples implémentation possible de notre WalletRepository. Nous pouvons très bien imaginer plus tard mettre en place un LocalStorageWalletRepository ou bien un SupabaseWalletRepostory.

Vous pouvez consulter mon répertoire public de broney sur GitHub pour voir mon implémentation de ces 2 repository et notamment de comment j'ai adapté ma série de test pour garantir leur bon fonctionnement.

User Interface

La partie user interface est composée de tous les adaptateurs qui constituent les points d'entrée de l'application. Les utilisateurs utilisent ces adaptateurs pour pouvoir interagir avec le coeur de l'application. Dans notre cas nous allons régulièrement utiliser des stores en utilisant la libraire Zustand. Il s'agit d'une libraire JS minimaliste pour la gestion d'états (une solution plus complexe serait par exemple Redux).

Voyons voir comment articuler notre store Zustand pour permettre à l'utilisateur d'interagir avec le coeur de l'application.

import { createStore } from 'zustand/vanilla'
import { InMemoryWalletRepository } from '../infrastructure/in-memory-wallet.repository'
import { WalletService } from '../domain/wallet.service'
import { Wallet } from '../domain/wallet'

const repository = new InMemoryWalletRepository()
const service = new WalletService(repository)

type States = {
	wallets: Wallet[]
	currentWallet: Wallet | undefined
}

type Actions = {
	load: () => void
	setCurrentWallet: (wallet: Wallet) => void
	getWallet: (walletId: string) => void
	createWallet: (wallet: Wallet) => void
	updateWallet: (wallet: Wallet) => void
	deleteWallet: (walletId: string) => void
}

export const walletStore = createStore()((set) => ({
	wallets: [],
	currentWallet: undefined,

	load: async () => {
		const allWallets = await service.getAll()
		set({ wallets: allWallets })
	},

	setCurrentWallet: (wallet) => set({ currentWallet: wallet }),

	getWallet: async (walletId: string) => {
		const wallet = await service.get(walletId)
		set({ currentWallet: wallet })
	},

	createWallet: async (wallet: Wallet) => {
		const newWallet = await service.create(wallet)
		set((state) => ({ wallets: [...state.wallets, newWallet] }))
	},

	updateWallet: async (wallet: Wallet) => {
		const updatedWallet = await service.update(wallet)
		set((state) => ({
			wallets: state.wallets.map((w) => (w.id === updatedWallet.id ? updatedWallet : w)),
			currentWallet: state.currentWallet?.id === updatedWallet.id ? updatedWallet : state.currentWallet,
		}))
	},

	deleteWallet: async (walletId: string) => {
		await service.delete(walletId)
		set((state) => ({
			wallets: state.wallets.filter((w) => w.id !== walletId),
			currentWallet: state.currentWallet?.id === walletId ? undefined : state.currentWallet,
		}))
	},
}))


Avec ce store on remarque qu'on va pouvoir facilement, dans n'importe quel environnement JavaScript, charger, définir, récupérer, créer, mettre à jour et supprimer des portefeuilles, tout en maintenant un état global pour l'ensemble des portefeuilles et du portefeuille courant.

Conclusion

Nous avons maintenant terminé ce deuxième article de cette série sur le développement d'une application web et mobile avec l'Architecture Hexagonale et le partage de la logique métier et des composants UI.

Dans cette deuxième partie nous avons vu comment travailler en TDD et surtout comment écrire de la logique métier sans avoir à ouvrir une quelque interface à l'exception du terminal pour les retours de tests.

Nous avons également eu un aperçu de comment nous allons interagir avec nos applications avec le coeur de l'application, via notre store Zustand. Nous irons plus loin à ce sujet dans le prochain article, la troisième partie : Partager de la logique métier et des composants entre le Web et le Mobile.

Échangeons sur votre projet !

Application web
Application mobile
Logiciel métier
Nous contacter

Simulateur

Bienvenue dans le
simulateur d’estimation

Sélectionnez
vos besoins

Sélectionnez un ou plusieurs choix

Définissez les
fonctionnalités

Sélectionnez un ou plusieurs choix

Dernière
étape !

Renseignez votre adresse mail pour recevoir l’estimation !
Obtenez l’estimation
Précédent
Suivant

Bravo ! Vous avez terminé
l’estimation de votre future app !

Vous recevrez dans votre boite mail l’estimation personnalisé. Une estimation vous offre la possibilité de vous projeter dans un budget, vous permettant ainsi de planifier en toute confiance. Néanmoins, chez Yield, nous adoptons une approche agile, prêts à remettre en question et ajuster nos évaluations en fonction de l'évolution de vos besoins et des spécificités de votre projet.
Retour au site
Oops! Something went wrong while submitting the form.