paint-brush
Tester une architecture propre dans une application frontale - Cela a-t-il un sens ?par@playerony
10,637 lectures
10,637 lectures

Tester une architecture propre dans une application frontale - Cela a-t-il un sens ?

par Paweł Wojtasiński21m2023/05/01
Read on Terminal Reader

Trop long; Pour lire

Les développeurs frontend sont confrontés au défi de créer des architectures évolutives et maintenables. De nombreuses idées architecturales proposées n'ont peut-être jamais été mises en œuvre dans des environnements de production réels. Cet article vise à fournir aux développeurs frontend les outils dont ils ont besoin pour naviguer dans le monde en constante évolution du développement de sites Web.
featured image - Tester une architecture propre dans une application frontale - Cela a-t-il un sens ?
Paweł Wojtasiński HackerNoon profile picture

À mesure que le paysage numérique évolue, la complexité des sites Web modernes évolue également. Avec une demande croissante pour une meilleure expérience utilisateur et des fonctionnalités avancées, les développeurs frontaux sont confrontés au défi de créer des architectures évolutives, maintenables et efficaces.


Parmi la pléthore d'articles et de ressources disponibles sur l'architecture frontale, un nombre important se concentre sur l'architecture propre et son adaptation. En fait, plus de 50% des près de 70 articles interrogés traitent de l'architecture propre dans le contexte du développement front-end.


Malgré la richesse des informations, un problème flagrant persiste : bon nombre des idées architecturales proposées n'ont peut-être jamais été mises en œuvre dans des environnements de production réels. Cela soulève des doutes quant à leur efficacité et leur applicabilité dans des scénarios pratiques.


Poussé par cette préoccupation, j'ai entrepris un voyage de six mois pour mettre en œuvre l'architecture propre sur le front-end, me permettant de confronter les réalités de ces idées et de séparer le bon grain de l'ivraie.


Dans cet article, je partagerai mes expériences et mes idées sur ce voyage, offrant un guide complet sur la façon de mettre en œuvre avec succès une architecture propre sur le frontend.


En mettant en lumière les défis, les meilleures pratiques et les solutions du monde réel, cet article vise à fournir aux développeurs frontaux les outils dont ils ont besoin pour naviguer dans le monde en constante évolution du développement de sites Web.

Cadres

Dans l'écosystème numérique en évolution rapide d'aujourd'hui, les développeurs n'ont que l'embarras du choix en matière de frameworks frontaux. Cette abondance d'options résout de nombreux problèmes et simplifie le processus de développement.


Cependant, cela conduit également à des débats sans fin entre développeurs, chacun affirmant que son framework préféré est supérieur aux autres. La vérité est que, dans notre monde en évolution rapide, de nouvelles bibliothèques JavaScript émergent quotidiennement et des frameworks sont introduits presque tous les mois.


Pour maintenir la flexibilité et l'adaptabilité dans un environnement aussi dynamique, nous avons besoin d'une architecture qui transcende les cadres et les technologies spécifiques.


Ceci est particulièrement crucial pour les sociétés de produits ou les contrats à long terme qui impliquent une maintenance, où l'évolution des tendances et les avancées technologiques doivent être prises en compte.


Être indépendant des détails, tels que les frameworks, nous permet de nous concentrer sur le produit sur lequel nous travaillons et de nous préparer aux changements qui peuvent survenir au cours de son cycle de vie.


Ne craignez rien; cet article vise à apporter une réponse à ce dilemme.

Coopération d'équipe complète

Dans ma quête pour mettre en œuvre l'architecture propre sur le frontend, j'ai travaillé en étroite collaboration avec plusieurs développeurs fullstack et backend pour m'assurer que l'architecture serait compréhensible et maintenable, même pour ceux qui ont une expérience minimale du frontend.


Ainsi, l'une des principales exigences de notre architecture est son accessibilité pour les développeurs backend qui ne connaissent peut-être pas bien les subtilités du frontend, ainsi que pour les développeurs fullstack qui n'ont peut-être pas une expertise approfondie du frontend.


En favorisant une coopération transparente entre les équipes frontend et backend, l'architecture vise à combler le fossé et à créer une expérience de développement unifiée.

Fondements théoriques

Malheureusement, pour construire des trucs géniaux, nous devons acquérir un savoir-faire de base. Une compréhension claire des principes sous-jacents facilitera non seulement le processus de mise en œuvre, mais garantira également que l'architecture respecte les meilleures pratiques en matière de développement de logiciels.


Dans cette section, nous présenterons trois concepts clés qui forment la base de notre approche architecturale : les principes SOLID , l'architecture propre (qui provient en fait des principes SOLID) et la conception atomique . Si vous vous sentez fortement à propos de ces domaines, vous pouvez ignorer cette section.

Principes SOLIDES

SOLID est un acronyme représentant cinq principes de conception qui guident les développeurs dans la création de logiciels évolutifs, maintenables et modulaires :


  • Principe de responsabilité unique (SRP) : ce principe stipule qu'une classe ne doit avoir qu'une seule raison de changer, ce qui signifie qu'elle doit avoir une seule responsabilité. En adhérant au SRP, les développeurs peuvent créer un code plus ciblé, maintenable et testable.


  • Principe Ouvert/Fermé (OCP) : Selon l'OCP, les entités logicielles doivent être ouvertes pour extension mais fermées pour modification. Cela signifie que les développeurs doivent pouvoir ajouter de nouvelles fonctionnalités sans modifier le code existant, réduisant ainsi le risque d'introduire des bogues.


  • Principe de substitution de Liskov (LSP) : LSP affirme que les objets d'une classe dérivée doivent pouvoir remplacer les objets de la classe de base sans affecter l'exactitude du programme. Ce principe favorise le bon usage de l'héritage et du polymorphisme.


  • Interface Segregation Principle (ISP) : ISP insiste sur le fait que les clients ne doivent pas être contraints de dépendre d'interfaces qu'ils n'utilisent pas. En créant des interfaces plus petites et plus ciblées, les développeurs peuvent assurer une meilleure organisation et maintenabilité du code.


  • Principe d'inversion de dépendance (DIP) : DIP encourage les développeurs à s'appuyer sur des abstractions plutôt que sur des implémentations concrètes. Ce principe favorise une base de code plus modulaire, testable et flexible.


Si vous souhaitez approfondir ce sujet, ce que je vous encourage vivement à faire, alors pas de problème. Cependant, pour l'instant, ce que j'ai présenté est suffisant pour aller plus loin.


Et que nous apporte SOLID dans cet article ?

Architecture épurée

Robert C. Martin, basé sur les principes SOLID et sa vaste expérience dans le développement de diverses applications, a proposé le concept de Clean Architecture. Lors de la discussion de ce concept, le diagramme ci-dessous est souvent référencé pour représenter visuellement sa structure :



Ainsi, l'architecture propre n'est pas un nouveau concept ; il a été largement utilisé dans divers paradigmes de programmation, y compris la programmation fonctionnelle et le développement backend.


Des bibliothèques comme Lodash et de nombreux frameworks backend ont adopté cette approche architecturale, ancrée dans les principes SOLID.


L'architecture propre met l'accent sur la séparation des préoccupations et la création de couches indépendantes et testables au sein d'une application, dans le but principal de rendre le système facile à comprendre, à entretenir et à modifier.


L'architecture est organisée en cercles ou couches concentriques ; chacun ayant des limites, des dépendances et des responsabilités claires :


  • Entités : il s'agit des principaux objets métier et règles de l'application. Les entités sont généralement des objets simples représentant les concepts essentiels ou les structures de données du domaine, tels que les utilisateurs, les produits ou les commandes.


  • Cas d'utilisation : Également appelés Interacteurs, les Cas d'utilisation définissent les règles métier spécifiques à l'application et orchestrent l'interaction entre les Entités et les systèmes externes. Les cas d'utilisation sont responsables de la mise en œuvre des fonctionnalités de base de l'application et doivent être indépendants des couches externes.


  • Adaptateurs d'interface : ces composants agissent comme un pont entre les couches interne et externe, convertissant les données entre le cas d'utilisation et les formats système externes. Les adaptateurs d'interface incluent des référentiels, des présentateurs et des contrôleurs, qui permettent à l'application d'interagir avec des bases de données, des API externes et des cadres d'interface utilisateur.


  • Frameworks et pilotes : cette couche la plus externe comprend les systèmes externes, tels que les bases de données, les frameworks d'interface utilisateur et les bibliothèques tierces. Les frameworks et les pilotes sont chargés de fournir l'infrastructure requise pour exécuter l'application et implémenter les interfaces définies dans les couches internes.


L'architecture propre favorise le flux de dépendances des couches externes vers les couches internes, garantissant que la logique métier de base reste indépendante des technologies ou des cadres spécifiques utilisés.


Il en résulte une base de code flexible, maintenable et testable qui peut facilement s'adapter à l'évolution des exigences ou des piles technologiques.

Conception atomique

Atomic Design est une méthodologie qui organise les composants de l'interface utilisateur en décomposant les interfaces en leurs éléments les plus élémentaires, puis en les réassemblant dans des structures plus complexes. Brad Frost a introduit le concept pour la première fois en 2008 dans un article intitulé "Atomic Design Methodology".


Voici un graphique montrant le concept d'Atomic Design :



Il se compose de cinq niveaux distincts :


  • Atomes : les plus petites unités indivisibles de l'interface, telles que les boutons, les entrées et les étiquettes.


  • Molécules : Groupes d'atomes qui fonctionnent ensemble, formant des composants d'interface utilisateur plus complexes comme des formulaires ou des barres de navigation.


  • Organismes : combinaisons de molécules et d'atomes qui créent des sections distinctes de l'interface, telles que des en-têtes ou des pieds de page.


  • Modèles : représentent la mise en page et la structure d'une page, fournissant un squelette pour le placement d'organismes, de molécules et d'atomes.


  • Pages : instances de modèles remplis de contenu réel, présentant l'interface finale.


En adoptant Atomic Design, les développeurs peuvent bénéficier de plusieurs avantages, tels que la modularité, la réutilisabilité et une structure claire pour les composants de l'interface utilisateur, car cela nous oblige à suivre l'approche Design System, mais ce n'est pas le sujet de cet article, alors passez à autre chose.

Étude de cas : NotionLingo

Afin de développer une perspective bien informée sur l'architecture propre pour le développement frontend, je me suis lancé dans un voyage pour créer une application. Sur une période de six mois, j'ai acquis des connaissances et une expérience précieuses tout en travaillant sur ce projet.


Par conséquent, les exemples fournis tout au long de cet article s'appuient sur mon expérience pratique avec l'application. Pour maintenir la transparence, tous les exemples sont dérivés d'un code accessible au public.


Vous pouvez explorer le résultat final en visitant le référentiel à https://github.com/Levofron/NotionLingo .

Mise en œuvre d'une architecture propre

Comme mentionné précédemment, de nombreuses implémentations de Clean Architecture sont disponibles en ligne. Cependant, quelques éléments communs peuvent être identifiés dans ces implémentations :


  • Couche de domaine : le cœur de notre application, englobant les modèles, les cas d'utilisation et les opérations liés à l'entreprise.


  • Couche API : responsable de l'interaction avec les API du navigateur.


  • Couche de référentiel : sert de pont entre les couches de domaine et d'API, fournissant un espace pour mapper les types d'API à nos types de domaine.


  • Couche UI : accueille nos composants, formant l'interface utilisateur.


En comprenant ces points communs, nous pouvons apprécier la structure fondamentale de l'architecture propre et l'adapter à nos besoins spécifiques.

Domaine

La partie centrale de notre application contient :


  • Cas d'utilisation : les cas d'utilisation décrivent les règles métier pour diverses opérations, telles que l'enregistrement, la mise à jour et la récupération de données. Par exemple, un cas d'utilisation peut impliquer la récupération d'une liste de mots à partir de Notion ou l'augmentation de la séquence quotidienne de l'utilisateur pour les mots appris.


    Essentiellement, les cas d'utilisation gèrent les tâches et les processus de l'application d'un point de vue commercial, garantissant que le système fonctionne conformément aux objectifs souhaités.


  • Modèles : les modèles représentent les entités métier au sein de l'application. Ceux-ci peuvent être définis à l'aide d'interfaces TypeScript, en veillant à ce qu'ils correspondent aux besoins et aux exigences de l'entreprise.


    Par exemple, si un cas d'utilisation implique d'extraire une liste de mots de Notion, vous auriez besoin d'un modèle pour décrire avec précision la structure de données de cette liste, en respectant les règles et contraintes métier appropriées.


  • Opérations : Parfois, définir certaines tâches comme des cas d'utilisation peut ne pas être faisable, ou vous pouvez créer des fonctions réutilisables qui peuvent être utilisées dans plusieurs parties de votre domaine. Par exemple, si vous avez besoin d'écrire une fonction pour rechercher un mot Notion par son nom, c'est là que ces opérations doivent résider.


    Les opérations sont utiles pour encapsuler une logique spécifique à un domaine qui peut être partagée et utilisée dans divers contextes au sein de l'application.


  • Interfaces du référentiel : Les cas d'utilisation nécessitent un moyen d'accéder aux données. Conformément au principe d'inversion des dépendances, la couche domaine ne doit dépendre d'aucune autre couche (alors que les autres couches en dépendent); par conséquent, cette couche définit les interfaces pour les référentiels.


    Il est important de noter qu'il spécifie les interfaces, pas les détails d'implémentation. Les référentiels eux-mêmes utilisent le modèle de référentiel qui est indépendant de la source de données réelle et met l'accent sur la logique de récupération ou d'envoi de données vers et depuis ces sources.


    Il est crucial de mentionner qu'un seul référentiel peut implémenter plusieurs API et qu'un seul cas d'utilisation peut utiliser plusieurs référentiels.

API

Cette couche est responsable de l'accès aux données et peut communiquer avec diverses sources selon les besoins. Considérant que nous développons une application frontale, cette couche servira principalement de wrapper pour les API du navigateur.


Cela inclut les API pour REST, le stockage local, IndexedDB, la synthèse vocale, etc.


Il est important de noter que si vous souhaitez générer des types OpenAPI et des clients HTTP, la couche API est l'endroit idéal pour les mettre. Dans cette couche, nous avons :


  • Adaptateur API : L'adaptateur API est un adaptateur spécialisé pour les API de navigateur utilisées dans notre application. Ce composant gère les appels REST et la communication avec la mémoire de l'application ou toute autre source de données que vous souhaitez utiliser.


    Vous pouvez même créer et implémenter votre propre système de stockage d'objets si vous le souhaitez. En disposant d'un adaptateur d'API dédié, vous pouvez maintenir une interface cohérente pour interagir avec diverses sources de données, ce qui facilite leur mise à jour ou leur modification selon les besoins.


  • Types : C'est un endroit pour tous les types liés à votre API. Ces types ne sont pas directement liés au domaine mais servent de descriptions des réponses brutes reçues de l'API. Dans la couche suivante, ces types seront essentiels pour une cartographie et un traitement appropriés.

Dépôt

La couche de référentiel joue un rôle crucial dans l'architecture de l'application en gérant l'intégration de plusieurs API, en mappant les types spécifiques aux API aux types de domaine et en incorporant des opérations de transformation des données.


Si vous souhaitez combiner l'API de synthèse vocale avec le stockage local, par exemple, c'est l'endroit idéal pour le faire. Cette couche contient :


  • Implémentation du référentiel : Ce sont des implémentations concrètes des interfaces déclarées dans la couche domaine. Ils sont capables de travailler avec plusieurs sources de données, garantissant ainsi flexibilité et adaptabilité au sein de l'application.


  • Opérations : elles peuvent être appelées mappeurs, transformateurs ou assistants. Dans ce contexte, les opérations sont un terme approprié. Ce répertoire contient toutes les fonctions chargées de mapper les réponses API brutes à leurs types de domaine correspondants, garantissant que les données sont correctement structurées pour une utilisation dans l'application.

Adaptateur


La couche adaptatrice est chargée d'orchestrer les interactions entre ces couches et de les lier entre elles. Cette couche ne contient que des modules responsables de :


  • Injection de dépendance : la couche adaptateur gère les dépendances entre les couches API, référentiel et domaine. En gérant l'injection de dépendances, la couche adaptateur assure une séparation nette des problèmes et favorise une réutilisation efficace du code.


  • Organisation des modules : La couche Adaptateur organise l'application en modules en fonction de leurs fonctionnalités (par exemple, stockage local, REST, synthèse vocale, Supabase). Chaque module encapsule une fonctionnalité spécifique, fournissant une structure propre et modulaire pour l'application.


  • Création d'actions : la couche Adaptateur crée des actions en combinant les cas d'utilisation de la couche Domaine avec les référentiels appropriés. Ces actions servent de points d'entrée pour que l'application interagisse avec les couches sous-jacentes.

Présentation

La couche de présentation est chargée de rendre l'interface utilisateur (UI) et de gérer les interactions de l'utilisateur avec l'application. Il exploite l'adaptateur, le domaine et les couches partagées pour créer une interface utilisateur fonctionnelle et interactive.


La couche de présentation utilise la méthodologie Atomic Design pour organiser ses composants, résultant en une application évolutive et maintenable. Cependant, cette couche ne sera pas l'objet principal de cet article, car ce n'est pas le sujet principal en termes de mise en œuvre de l'architecture propre.

partagé

Un emplacement désigné est nécessaire pour tous les éléments communs, tels que les utilitaires centralisés, les configurations et la logique partagée. Cependant, nous n'approfondirons pas trop cette couche dans cet article.


Il convient de le mentionner simplement pour comprendre comment les composants communs sont gérés et partagés dans l'ensemble de l'application.

Stratégies de test pour chaque couche

Maintenant, avant de plonger dans le codage, il est essentiel de discuter des tests. Assurer la fiabilité et l'exactitude de votre application est vital, et il est crucial de mettre en œuvre une stratégie de test robuste pour chaque couche de l'architecture.


  • Couche de domaine : les tests unitaires sont la principale méthode de test de la couche de domaine. Concentrez-vous sur le test des modèles de domaine, des règles de validation et de la logique métier, en vous assurant qu'ils se comportent correctement dans diverses conditions. Adoptez le développement piloté par les tests (TDD) pour piloter la conception de vos modèles de domaine et confirmer que votre logique métier est solide.


  • Couche API : testez la couche API à l'aide de tests d'intégration. Ces tests doivent s'assurer que l'API interagit correctement avec les services externes et que les réponses sont correctement formatées. Utilisez des outils tels que des cadres de test automatisés, tels que Jest, pour simuler des appels d'API et valider les réponses.


  • Couche référentiel : Pour la couche référentiel, vous pouvez utiliser une combinaison de tests unitaires et d'intégration. Les tests unitaires peuvent être utilisés pour tester des méthodes de référentiel individuelles, tandis que les tests d'intégration doivent se concentrer sur la vérification que les référentiels interagissent correctement avec leurs API.


  • Couche adaptatrice : Les tests unitaires conviennent pour tester la couche adaptatrice. Ces tests doivent garantir que les adaptateurs injectent correctement les dépendances et gèrent les transformations de données entre les couches. Se moquer des dépendances, telles que les couches d'API ou de référentiel, peut aider à isoler la couche d'adaptateur pendant les tests.


En mettant en œuvre une stratégie de test complète pour chaque couche de l'architecture, vous pouvez garantir la fiabilité, l'exactitude et la maintenabilité de votre application tout en réduisant la probabilité d'introduire des bogues pendant le développement.


Cependant, si vous construisez une petite application, des tests d'intégration sur la couche adaptateur devraient suffire.

Codons quelque chose

Très bien, maintenant que vous avez une solide compréhension de l'architecture propre et que vous avez peut-être même formé votre propre opinion à ce sujet, approfondissons un peu et explorons du code réel.


Gardez à l'esprit que je ne présenterai qu'un exemple simple ici; cependant, si vous êtes intéressé par des exemples plus détaillés, n'hésitez pas à explorer mon référentiel GitHub mentionné au début de cet article.


Dans la "vraie vie", l'architecture propre brille vraiment dans les grandes applications au niveau de l'entreprise, alors qu'elle peut être exagérée pour les petits projets. Cela dit, venons-en au fait.


En utilisant mon application comme exemple, je vais montrer comment effectuer un appel d'API pour récupérer des suggestions de dictionnaire pour un mot donné. Ce point de terminaison d'API particulier récupère une liste de significations et d'exemples en grattant deux sites Web.


D'un point de vue commercial, ce point de terminaison est crucial pour la vue "Rechercher un mot", qui permet aux utilisateurs de rechercher un mot spécifique. Une fois que l'utilisateur a trouvé le mot et s'est connecté, il peut ajouter les informations récupérées sur le Web à sa base de données Notion.

Structure des dossiers

Pour commencer, nous devons établir une structure de dossiers qui reflète fidèlement les couches dont nous avons discuté précédemment. La structure doit ressembler à ce qui suit :


 client ├── adapter ├── api ├── domain ├── presentation ├── repository └── shared


Le répertoire client sert un objectif similaire au dossier "src" dans de nombreux projets. Dans ce projet Next.js spécifique, j'ai adopté la convention consistant à nommer le dossier frontal en tant que "client" et le dossier principal en tant que "serveur".


Cette approche permet une distinction claire entre les deux composants principaux de l'application.

Sous-répertoires

Choisir la bonne structure de dossiers pour votre projet est en effet une décision cruciale qui doit être prise tôt dans le processus de développement. Différents développeurs ont leurs propres préférences et approches en matière d'organisation des ressources.


Certains peuvent regrouper les ressources par noms de page, d'autres peuvent suivre les conventions de dénomination des sous-répertoires générées par OpenAPI, et encore, d'autres peuvent penser que leur application est trop petite pour justifier l'une ou l'autre de ces solutions.


La clé est de choisir une structure qui correspond le mieux aux besoins spécifiques et à l'échelle de votre projet tout en maintenant une organisation claire et maintenable des ressources.


Je suis dans le troisième groupe, donc ma structure ressemble à ceci:


 client ├── adapter │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ └── supabase ├── api │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ └── supabase ├── domain │ ├── local-storage │ ├── rest │ ├── speech-synthesis │ ├── supabase └── repository ├── local-storage ├── rest ├── speech-synthesis └── supabase


J'ai décidé d'omettre les couches partagées et de présentation dans cet article, car je pense que ceux qui veulent approfondir peuvent se référer à mon référentiel pour plus d'informations. Passons maintenant à quelques exemples de code pour illustrer comment l'architecture propre peut être appliquée dans une application frontale.

Définition de domaine

Considérons nos besoins. En tant qu'utilisateur, j'aimerais recevoir une liste de suggestions, y compris leur signification et des exemples. Par conséquent, une seule suggestion de dictionnaire peut être modélisée comme suit :


 interface DictionarySuggestion { example: string; meaning: string; }


Maintenant que nous avons décrit une suggestion de dictionnaire unique, il est important de mentionner que parfois le mot obtenu grâce au web scraping diffère ou est corrigé par rapport à ce que l'utilisateur a tapé. Pour tenir compte de cela, nous utiliserons la version corrigée plus tard dans notre application.


Par conséquent, nous devons définir une interface qui inclut une liste de suggestions de dictionnaires et de corrections de mots. L'interface finale ressemble à ceci :


 export interface DictionarySuggestions { suggestions: DictionarySuggestion[]; word: string; }


Nous exportons cette interface, c'est pourquoi le mot-clé export est inclus.

Interface du référentiel

Nous avons notre modèle, et maintenant il est temps de l'utiliser.


 import { DictionarySuggestions } from './rest.models'; export interface RestRepository { getDictionarySuggestions: (word: string) => Promise<DictionarySuggestions | null>; }


À ce stade, tout devrait être clair. Il est important de noter que nous ne parlons pas du tout de l'API ici ! La structure du référentiel lui-même est assez simple : juste un objet avec quelques méthodes, où chaque méthode renvoie des données d'un type spécifique de manière asynchrone.


N'oubliez pas que le référentiel renvoie toujours les données au format du modèle de domaine.

Cas d'utilisation

Maintenant, définissons notre règle métier comme un cas d'utilisation. Le code ressemble à ceci :


 export type GetDictionarySuggestionsUseCaseUseCase = UseCaseWithSingleParamAndPromiseResult< string, DictionarySuggestions | null >; export const getDictionarySuggestionsUseCase = ( restRepository: RestRepository, ): GetDictionarySuggestionsUseCaseUseCase => ({ execute: (word) => restRepository.getDictionarySuggestions(word), });


La première chose à noter est la liste des types communs utilisés pour définir les cas d'utilisation. Pour ce faire, j'ai créé un fichier use-cases.types.ts dans le répertoire du domaine :


 domain ├── local-storage ├── rest ├── speech-synthesis ├── supabase └── use-cases.types.ts


Cela me permet de partager facilement des types de cas d'utilisation entre mes sous-répertoires. La définition de UseCaseWithSingleParamAndPromiseResult ressemble à ceci :


 export interface UseCaseWithSingleParamAndPromiseResult<TParam, TResult> { execute: (param: TParam) => Promise<TResult>; }


Cette approche permet de maintenir la cohérence et la réutilisabilité des types de cas d'utilisation à travers la couche de domaine.


Vous vous demandez peut-être pourquoi nous avons besoin de la fonction execute . Ici, nous avons une usine qui renvoie le cas d'utilisation réel.


Ce choix de conception est dû au fait que nous ne voulons pas référencer l'implémentation du référentiel directement dans le code du cas d'utilisation, ni que le référentiel soit utilisé par une importation. Cette approche nous permet d'appliquer facilement l'injection de dépendances par la suite.


En utilisant le modèle d'usine et la fonction execute , nous pouvons séparer les détails d'implémentation du référentiel du code de cas d'utilisation, ce qui améliore la modularité et la maintenabilité de l'application.


Cette approche suit le principe d'inversion de dépendance, où la couche de domaine ne dépend d'aucune autre couche, et elle permet une plus grande flexibilité lorsqu'il s'agit d'échanger différentes implémentations de référentiel ou de modifier l'architecture de l'application.

Définition de l'API

Commençons par définir notre interface :


 export interface RestApi { getDictionarySuggestions: (word: string) => Promise<AxiosResponse<DictionarySuggestions>>; }


Comme vous pouvez le voir, la définition de cette fonction dans l'interface ressemble beaucoup à celle du référentiel. Étant donné que le type de domaine décrit déjà la réponse, il n'est pas nécessaire de recréer le même type.


Il est important de noter que notre API renvoie des données brutes, c'est pourquoi nous renvoyons le AxiosResponse<DictionarySuggestions> complet. Ce faisant, nous maintenons une séparation claire entre les couches API et domaine, permettant une plus grande flexibilité dans le traitement et la transformation des données.


L'implémentation de cette API ressemble à ceci :


 export const getRestApi = (axiosInstance: AxiosInstance): RestApi => ({ getDictionarySuggestions: async (word: string) => { const encodedCurrentDate = encodeURIComponent(word); const response = await axiosInstance.get( `${RestEndpoints.GET_DICTIONARY_SUGGESTIONS}?word=${encodedCurrentDate}`, ); return response; } });


À ce stade, les choses deviennent plus intéressantes. Le premier aspect important à aborder est l'injection de notre axiosInstance . Cela rend notre code très flexible et nous permet de construire facilement des tests solides. C'est également l'endroit où nous gérons l'encodage ou l'analyse des paramètres de requête.


Cependant, vous pouvez également effectuer d'autres actions ici, telles que le découpage de la chaîne d'entrée. En injectant axiosInstance , nous maintenons une séparation claire des préoccupations et veillons à ce que la mise en œuvre de l'API soit adaptable à différents scénarios ou changements dans les services externes.

Implémentation du référentiel

Comme notre interface est déjà définie par le domaine, il ne nous reste plus qu'à implémenter notre référentiel. Ainsi, l'implémentation finale ressemble à ceci :

 export const getRestRepository = (restApi: RestApi): RestRepository => ({ getDictionarySuggestions: async (word) => { const { data } = await restApi.getDictionarySuggestions(word); if (!data?.suggestions?.length) { return null; } return formatDictionarySuggestions(data); } });


Un aspect important à mentionner est lié aux API. Notre getRestRepository nous permet de passer un restApi préalablement défini. Ceci est avantageux car, comme mentionné précédemment, cela permet un test plus facile. Nous pouvons brièvement examiner formatDictionarySuggestions :


 export const formatDictionarySuggestions = ({ suggestions, word, }: DictionarySuggestions): DictionarySuggestions => { const cleanedWord = cleanUpString(word); const cleanedSuggestions = suggestions.map((_suggestion) => { const cleanedMeaning = cleanUpString(_suggestion.meaning); const cleanedExample = cleanUpString(_suggestion.example); return { meaning: cleanedMeaning, example: cleanedExample, }; }); return { word: cleanedWord, suggestions: cleanedSuggestions, }; };


Cette opération prend notre modèle DictionarySuggestions de domaine comme argument et effectue un nettoyage de chaîne, ce qui signifie supprimer les espaces inutiles, les sauts de ligne, les tabulations et les majuscules. C'est assez simple, sans complexités cachées.


Une chose importante à noter est qu'à ce stade, vous n'avez pas à vous soucier de l'implémentation de votre API. Pour rappel, le référentiel renvoie toujours les données dans le modèle de domaine ! Il ne peut en être autrement car cela enfreindrait le principe d'inversion des dépendances.


Et pour l'instant, notre couche de domaine ne dépend de rien de défini en dehors d'elle.

Adaptateur - Mettons tout cela ensemble

À ce stade, tout doit être implémenté et prêt pour l'injection de dépendances. Voici l'implémentation finale du module rest :


 import { getRestRepository } from '@repository/rest/rest.repository'; import { getRestApi } from '@api/rest/rest.api'; import { getDictionarySuggestionsUseCase } from '@domain/rest/rest.use-cases'; import { axiosInstance } from '@shared/axios.instance'; const restApi = getRestApi(axiosInstance); const restRepository = getRestRepository(restApi); export const restModule = { getDictionarySuggestions: getDictionarySuggestionsUseCase(restRepository).execute, };


C'est exact! Nous sommes passés par le processus de mise en œuvre des principes de l'architecture propre sans être liés à un cadre spécifique. Cette approche garantit que notre code est adaptable, ce qui facilite le changement de framework ou de bibliothèque si nécessaire.


En ce qui concerne les tests, consulter le référentiel est un excellent moyen de comprendre comment les tests sont implémentés et organisés dans cette architecture.


Avec une base solide en Clean Architecture, vous pouvez écrire des tests complets qui couvrent divers scénarios, rendant votre application plus robuste et plus fiable.


Comme démontré, suivre les principes de l'architecture propre et séparer les préoccupations conduit à une structure d'application maintenable, évolutive et testable.


Cette approche facilite finalement l'ajout de nouvelles fonctionnalités, la refactorisation du code et le travail en équipe sur un projet, garantissant ainsi le succès à long terme de votre application.

Présentation

Dans l'exemple d'application, React est utilisé pour la couche de présentation. Dans le répertoire de l'adaptateur, il existe un fichier supplémentaire appelé hooks.ts qui gère l'interaction avec le module de repos. Le contenu de ce fichier est le suivant :


 import { restModule } from '@adapter/rest/rest.module'; import { useAxios } from '@shared/hooks'; export const useDictionarySuggestions = () => { const { data, error, isLoading, mutate } = useAxios(restModule.getDictionarySuggestions); return { dictionarySuggestions: data, getDictionarySuggestions: mutate, dictionarySuggestionsError: error, isDictionarySuggestionsLoading: isLoading, }; };


Cette implémentation facilite incroyablement le travail avec la couche de présentation. En utilisant le crochet useDictionarySuggestions , la couche de présentation n'a pas à se soucier de la gestion des mappages de données ou d'autres responsabilités qui ne sont pas liées à sa fonction principale.


Cette séparation des préoccupations permet de maintenir les principes de l'architecture propre, conduisant à un code plus gérable et maintenable.

Et après?

Avant tout, je vous encourage à vous plonger dans le code du référentiel GitHub fourni et à explorer sa structure.


Que pouvez vous faire d'autre? Le ciel est la limite! Tout dépend de vos besoins de conception spécifiques. Par exemple, vous pouvez envisager d'implémenter la couche de données en incorporant un magasin de données (Redux, MobX ou même quelque chose de personnalisé - cela n'a pas d'importance).


Alternativement, vous pouvez expérimenter différentes méthodes de communication entre les couches, comme l'utilisation de RxJS pour gérer la communication asynchrone avec le backend, ce qui peut impliquer des interrogations, des notifications push ou des sockets (essentiellement, être préparé pour n'importe quelle source de données).


Essentiellement, n'hésitez pas à explorer et à expérimenter à votre guise, tant que vous maintenez l'architecture en couches et que vous adhérez au principe de dépendance inverse. Assurez-vous toujours que le domaine est au cœur de votre conception.


Ce faisant, vous créerez une structure d'application flexible et maintenable qui peut s'adapter à divers scénarios et exigences.

Résumé

Dans cet article, nous nous sommes penchés sur le concept d'architecture propre dans le contexte d'une application d'apprentissage des langues construite à l'aide de React.


Nous avons souligné l'importance de maintenir une architecture en couches et de respecter le principe de dépendance inverse, ainsi que les avantages de séparer les préoccupations.


Un avantage significatif de Clean Architecture est sa capacité à vous permettre de vous concentrer sur l'aspect technique de votre application sans être lié à un framework spécifique. Cette flexibilité vous permet d'adapter votre application à divers scénarios et exigences.


Cependant, il y a quelques inconvénients à cette approche. Dans certains cas, suivre un modèle architectural strict peut entraîner une augmentation du code passe-partout ou une complexité accrue dans la structure du projet.


De plus, s'appuyer moins sur la documentation peut être à la fois un avantage et un inconvénient - bien que cela permette plus de liberté et de créativité, cela peut également entraîner une confusion ou une mauvaise communication entre les membres de l'équipe.


Malgré ces défis potentiels, la mise en œuvre d'une architecture propre peut être très bénéfique, en particulier dans le contexte de React, où il n'existe pas de modèle architectural universellement accepté.


Il est essentiel de considérer votre architecture au début d'un projet plutôt que de l'aborder après des années de galère.


Pour explorer un exemple concret d'architecture propre en action, n'hésitez pas à consulter mon référentiel sur https://github.com/Levofron/NotionLingo . Vous pouvez également me contacter sur les réseaux sociaux via les liens fournis sur mon profil.


Wow, c'est probablement l'article le plus long que j'ai jamais écrit. C'est incroyable !