7
minutes
Mis à jour le
28/1/2025


Share this post

Comment utiliser efficacement les design patterns dans votre développement frontend. À travers l'exemple du facade pattern, nous verrons une solution pratique pour gérer la complexité de vos applications, centraliser la logique et améliorer la maintenabilité.

#
Front-end
#
Design Pattern
#
Architecture Frontend
#
Angular
Dorian Frances
Software Engineer

Pourquoi l’architecture frontend mérite notre attention

Dans le monde du développement logiciel, lorsque nous parlons d'architecture de code, il nous est facile de trouver d'innombrables ressources sur internet discutant d'architecture backend. Les architectures frontend, elles, sont bien moins plébiscitées. Et pour cause, car en réalité, la majeure partie de notre logique métier se trouve effectivement du côté serveur de notre application. La partie client, elle, n'est censée représenter qu'une interface belle et intuitive auprès de l'utilisateur.

Cependant, le fait que la logique métier soit souvent moins présente côté client ne signifie pas que l'architecture de cette partie de l'application puisse être négligée. Au contraire, le frontend recèle souvent une complexité sous-estimée, incluant notamment des logiques d'affichage, de gestion des interactions ou même des fragments de logique métier. En tant que développeurs, il nous est essentiel de maintenir une réflexion critique sur cette architecture, afin d'assurer la qualité de notre application et, par conséquent, une meilleure expérience pour nos utilisateurs.

Les clés d’une architecture frontend efficace

Petit disclaimer : il n'existe pas d'architecture parfaite. C'est un point très important que je me dois d'énoncer dès lors que cet article a pour but de promouvoir un (pas le) pattern d'architecture. Chaque situation est différente, amenant par essence des problématiques et des solutions qui y répondent différentes.

Cependant, nous pourrions noter quelques points sur lesquels, à force d'itérations, la communauté s'accorde à définir comme importants lorsque l'on parle d'architecture :

  1. L'expérience développeur : une architecture sur laquelle les développeurs n'ont aucun (ou peu de) mal à travailler est indispensable.
  2. Scalabilité & Maintenabilité : il est important de noter qu'en tant que développeur/PO/PM, il nous est très difficile d'anticiper ce que sera le futur à très long terme de notre application. Cela ne veut pas dire qu'il faut commencer par implémenter des architectures trop complexes pour autant. L'important est de créer quelque chose de relativement évolutif, certes, mais également et surtout quelque chose que l'équipe maîtrise.
  3. Refléter le besoin métier : en réalité, c'est de ce critère que découlent les deux précédents. Une architecture qui suit le besoin métier, le parcours utilisateur, sera par essence plus scalable dès lors qu'elle évolue en même temps que le métier. Elle sera également plus compréhensible du point de vue des développeurs.

À noter que dans la suite de cet article, je donnerai certains exemples de code utilisant le framework Angular. Mais la plupart des concepts et exemples sont très aisément reproductibles sur d'autres environnements/langages.

Les pièges classiques de l’architecture frontend

Dans une application Angular classique amenée à évoluer constamment, il nous est très aisé de tomber dans le piège d'avoir des composants trop chargés, qui gèrent à la fois les appels API, la gestion de l'état des variables, la logique métier en plus de certains aspects de l'interface utilisateur. Cette surcharge rend les composants difficiles à lire, à maintenir et à tester.

S'ensuit un exemple pour illustrer ce propos :

  1. Trop de responsabilités : dans cet exemple, le composant gère les appels API (loadTasks et changeTaskStatus), la gestion de l'état et des erreurs (tasks, completedTasks, loading, error) ainsi que la logique métier (le filtrage des tâches et la mise à jour de leur statut).
  2. Duplication de code : la logique de filtrage est dupliquée à plusieurs endroits (applyFilter et updateTaskLists). Cette répétition rend le code non seulement long et redondant, mais cela le rend également propice à différents défauts si la mise à jour à un endroit est oubliée dans un autre !
  3. Couplage aux implémentations techniques : pour celles et ceux initiés au concept de clean architecture, je ne vous apprends rien en vous disant que coupler notre logique métier à un choix d'implémentation technique n'est pas idéal. Un changement relatif à l'implémentation technique utilisée pour communiquer avec notre API externe provoquerait davantage de rework et une perte de temps non négligeable (implémentation d'un cache, changement quelconque relatif à l'implémentation de notre API).
  4. Difficulté à tester : pour tester ce composant, nous devrions :
    1. mocker le service HTTP pour simuler les appels API
    2. simuler les changements de filterControl pour vérifier que le filtrage fonctionne
    3. vérifier les mises à jour de l'état (tasks, filteredUncompletedTasks, etc.).

Ce niveau de complexité rend les tests plus longs à écrire et plus fragiles car ils dépendent de nombreux détails techniques.

⭐ À noter que j'ai volontairement choisi un exemple surchargé pour démontrer l'importance (dans certains cas) de découpler nos composants et de séparer les différentes responsabilités de notre application.

Le facade pattern : notre allié pour un frontend clair et maintenable

Le facade pattern est une approche qui permet de simplifier la gestion de la complexité dans une application frontend. Il agit comme une interface centrale entre les composants, la logique métier et les différents services nécessaires au développement de nos fonctionnalités, offrant un moyen efficace de centraliser l'état, d'interagir avec des services externes (APIs) et de découpler les composants de l'implémentation technique sous-jacente.

Cacher la complexité

La façade sert de point d'entrée unique pour la gestion des données et des états. Les composants, souvent surchargés dans une architecture négligée, délèguent la logique métier à une façade et se concentrent sur leur rôle principal : la présentation de la donnée et les interactions avec l'utilisateur.

Le composant MyTaskPage est beaucoup plus simple. Tout la logique, qu'il s'agisse de gérer les tâches ou d'intéragir avec l'API, est cachée dans la façade.

Grâce à la façade, le composant se limite à deux responsabilités principales :

  • Récupérer les données ou l’état via des selectors exposés par la façade (tasksService.filteredUncompletedTasks).
  • Appeler des méthodes simplifiées pour déclencher des actions (tasksService.changeTaskStatus).
Centraliser la logique d'état et d'interaction API

Une façade regroupe toutes les opérations concernant :

  • L’état : Les tâches, les filtres, les indicateurs de chargement et les erreurs sont centralisés dans un seul objet (TasksState).
  • Les interactions avec les API : Toute communication avec des services externes passe par des appels encapsulés dans la façade.

Ici la façade :

  • Cache la logique API en gérant les appels HTTP via TasksApiClientService
  • Centralise l’état (les tâches, les filtres, les erreurs) et expose des données dérivées via des selectors (comme completedTasks et filteredUncompletedTasks).
  • Simplifie la gestion des erreurs et des statuts de chargement.
Découpler nos composants pour un code plus flexible

Un des grands avantages du façade pattern est le découplage entre l’interface utilisateur et la logique métier. Le composant n’a pas besoin de savoir d’où viennent les données, ni comment elles sont filtrées ou mises à jour. Ce découplage présente plusieurs bénéfices :

  • Les composants deviennent plus simples, plus lisibles et réutilisables.
  • Les changements dans l'implémentation de l'API ou de la logique métier n'impactent pas les composants.
  • Les tests deviennent plus facile à écrire, car les composants se limitent à consommer une API simplifiée (stubber les méthodes de notre façade peut nous permettre de réaliser des tests centrés sur l'UI de nos fonctionnalités).

⭐ Ici, le composant ne s’occupe ni de charger les tâches ni de filtrer les données. Il délègue tout cela à la façade, réduisant considérablement la complexité et le couplage.

Les erreurs courantes avec le facade pattern (et comment les corriger)

🔍 Créer une façade pour chaque service : Cela peut entraîner une sur-ingénierie et une augmentation de la complexité inutile. Le rôle d’une façade est de centraliser la logique complexe et de servir de point de coordination pour des composants impliquant plusieurs aspects (par exemple, un état global ou plusieurs appels API). Si notre service est simple (par exemple, il se contente de fournir une liste statique), ajouter une façade ne fait qu’alourdir inutilement l’architecture.

Solution : Appliquer le façade pattern uniquement dans les endroits stratégiques, là où la logique métier ou la gestion de l’état deviennent complexes.

🔍 Créer des façades trop génériques et sans focus : Une façade trop générique devient un goulot d’étranglement et un point unique de défaillance. En centralisant trop de responsabilités dans une seule classe, nous risquons de recréer le problème que l’on essayait de résoudre : un code complexe, difficile à tester et à maintenir.

Solution : Divisez les façades par “feature” ou domaine fonctionnel. Dans notre exemple :

  • Une façade pour gérer les tâches.
  • Nous pourrions imaginer une façade pour gérer des projets associés à ces tâches.

Cela permet une séparation claire des responsabilités et améliore la lisibilité et la maintenabilité du code.

🔍 Manquer de granularité dans les selectors : Ne pas exposer de selectors dérivés dans la façade oblige les composants à manipuler eux-mêmes les données.

Solution : Fournir des selectors spécifiques dans la façade pour répondre aux besoins du composant. Dans notre exemple, nous fournissons les selectors filteredUncompletedTasks, uncompletedTasks, etc.

🔍 Confusion entre façade et logique métier complexe : Ajouter dans la façade l’ensemble de la logique métier peut la rendre compliquée à maintenir.

Solution : La logique métier complexe devrait rester dans des services métiers dédiés. La façade ne fait que coordonner l’accès à ces services.

💡 Remarque : Vous vous dites peut-être qu’avec l’exemple que je vous ai donné, je ne respecte pas cette “règle” car une partie de ma logique métier est présente dans la façade (TasksService). J’ai fait le choix ici de ne pas externaliser la logique métier dans un autre service car je la considère comme assez simple pour ne pas complexifier encore davantage la lecture de la fonctionnalité.

Adoptez le facade pattern : un choix pragmatique pour nos projets

La remarque ci-dessus est, pour moi, quelque chose que l’on doit toujours garder en tête. Il existe de nombreuses manières d’architecturer notre code, certaines plus complexes que d’autres et répondant à des besoins spécifiques. Cependant, nous devons toujours rester pragmatiques en nous demandant si, oui ou non, nous avons réellement besoin d’introduire telle ou telle complexité au sein de notre produit.

En réalité, je fais un abus de langage lorsque je présente ce pattern comme de l’architecture. La réelle notion d’architecture réside dans la décision d’implémenter (ou non) ce pattern. C’est une affaire de choix.

💬 Architecture is about the important stuff. Whatever that is.
Martin Fowler -

La définition et le design d’un produit répondent à certaines problématiques qui viennent avec leurs propres contraintes. Ainsi, si ce modèle mental/pattern ne convient pas à nos besoins (les choses importantes), il n’est absolument pas impératif de le mettre en place. Mais je tenais quand même à vous le présenter : ça vous fait un nouvel outil dans votre boîte à outils 😉.

Ressources :