L’approche de conception en couches de Go
(jerf.org)- Comme le langage Go interdit strictement les références circulaires entre packages, il encourage naturellement une conception en couches (layered design)
- Cet article explique la structure en couches qu’un projet Go adopte presque inévitablement et soutient qu’elle est déjà largement valable sans imposer d’architecture supplémentaire
- En cas de dépendance circulaire, il propose des stratégies de refactoring concrètes et pragmatiques, étape par étape, pour la résoudre
- Chaque package est conçu comme une unité fonctionnelle autonome et significative, ce qui favorise les tests, la maintenance et la séparation en microservices
- En pratique, cette approche évite le problème classique du « je voulais une banane, on m’a livré la jungle » dans la conception du code
L’approche de conception en couches dans Go
Principes de base
- Go interdit les références circulaires entre packages
- Les relations d’import de tout programme Go doivent former un graphe orienté acyclique (DAG)
- Cette structure n’est pas un choix, mais une règle de conception imposée au niveau du langage
Formation automatique des couches de packages
- En excluant les packages externes, les packages internes d’un projet peuvent être automatiquement organisés en couches selon leur profondeur de référence
- Comme dans le schéma ci-dessous, on trouve tout en bas les packages utilitaires de base comme metrics, logging ou des structures de données communes
- Les packages de niveau supérieur viennent ensuite s’empiler progressivement en combinant les fonctionnalités
Caractéristiques de cette approche
- Les couches reposent non sur une abstraction hiérarchique, mais sur la direction des références
- Un package peut référencer plusieurs packages de niveau inférieur
- Des approches classiques comme MVC ou l’architecture hexagonale peuvent elles aussi être « appliquées » sur cette structure
→ à condition de toujours tenir compte des contraintes structurelles propres à Go
Stratégies pour résoudre les références circulaires
En cas de référence circulaire, il est recommandé d’essayer le refactoring dans l’ordre suivant :
1. Déplacer la fonctionnalité
- La méthode la plus recommandée
- Analyser avec précision la fonctionnalité à l’origine du cycle, puis la déplacer à l’endroit le plus logique
- Cette méthode est peu utilisée en pratique, mais c’est celle qui améliore le plus la clarté conceptuelle
2. Extraire les fonctionnalités communes dans un package séparé
- Déplacer dans un troisième package les types ou fonctions communs utilisés des deux côtés (
Username, par exemple) - Même si le package semble minuscule au départ, il faut l’extraire sans hésiter
→ avec le temps, il a de fortes chances de s’étoffer
3. Créer un package de composition de niveau supérieur
- Créer un troisième package chargé de combiner les deux packages en cycle
- Exemple : extraire la dépendance bidirectionnelle entre
CategoryetBlogPostvers un package supérieur
→ les packages inférieurs restent de simples structs, et la logique réelle est composée dans le package supérieur
4. Introduire une interface
- Remplacer la dépendance par une interface qui ne contient que les méthodes réellement nécessaires à une struct ou une fonction
- Cela permet de supprimer des dépendances inutiles et de faciliter les tests
- En revanche, un usage excessif peut au contraire compliquer la conception
5. Copier (Copy)
- Si l’élément dont on dépend est très petit, on peut le copier simplement
- Cela peut sembler violer le principe DRY, mais en pratique cela aide souvent à clarifier la conception
6. Fusionner dans un seul package
- Si aucune des méthodes précédentes n’est possible, fusionner les deux packages
- C’est acceptable tant que le package résultant ne devient pas trop gros
→ mais il faut éviter les fusions systématiques et décider avec prudence
Avantages pratiques de cette approche
- Chaque package constitue une unité fonctionnelle autonome et porteuse de sens, testable indépendamment
- Les références étant limitées à l’intérieur de chaque package, on peut comprendre un package sans devoir saisir l’ensemble du codebase
- Cette approche évite les connexions de dépendances globales non voulues (= le problème de la jungle) et pousse à n’utiliser que ce qui est nécessaire
- Elle permet aussi d’extraire facilement des microservices
→ la plupart des dépendances étant clairement définies
Conclusion
- Les contraintes de conception des packages en Go ne sont pas une gêne, mais un mécanisme qui pousse vers de bonnes pratiques de conception
- Même sans architecture particulière, la seule structure des références entre packages suffit à produire une conception robuste
- L’analyse fine des références circulaires et les stratégies de refactoring qui en découlent sont utiles non seulement en Go, mais aussi dans d’autres langages
4 commentaires
Au début, quand on code ça à l'arrache et que ça tourne, c'est amusant.
Mais dès qu'on commence à ajouter des tests,
on se met à se demander pourquoi on a fait ça comme ça.
L’expression « Je voulais une banane, mais on m’a livré la jungle » est vraiment très drôle.
L’une des choses les plus difficiles quand on développait avec Spring, c’était je crois les dépendances circulaires..
Cette frustration de les voir s’initialiser mutuellement à l’infini jusqu’à faire planter l’appli avec une fuite mémoire...
Avis Hacker News
Le fait de ne pas autoriser les dépendances circulaires est un excellent choix de conception lorsqu’on construit des programmes de grande taille
Excellent billet de blog
Technique bonus liée au conseil « déplacer vers un troisième package »
On dirait que c’est en train de lire un livre sur la méthode structurée de Yourdon
Les packages ne peuvent pas se référencer mutuellement de façon circulaire
Cela fait penser au concept concret de randomizer
Une caractéristique amusante de Golang est qu’on ne peut pas avoir de dépendances circulaires au niveau des packages, mais qu’on peut en avoir dans go.mod
Belle explication de la façon dont Jerf pense les packages et traite les dépendances circulaires