27 points par GN⁺ 2025-04-24 | 4 commentaires | Partager sur WhatsApp
  • 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 Category et BlogPost vers 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

 
bus710 2025-04-25

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.

 
bungker 2025-04-24

L’expression « Je voulais une banane, mais on m’a livré la jungle » est vraiment très drôle.

 
iwanhae 2025-04-24

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...

 
GN⁺ 2025-04-24
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

    • Cela force à bien séparer les responsabilités
    • Si une dépendance circulaire apparaît, c’est qu’il y a un problème de conception, et l’article explique bien comment le résoudre
    • Il arrive parfois de résoudre une dépendance circulaire en utilisant des pointeurs de fonction redéfinis par un autre package
    • J’aimerais que le compilateur Go fournisse des messages plus utiles lorsqu’on crée des dépendances circulaires
    • Actuellement, il donne la liste de tous les packages impliqués dans la boucle, ce qui peut être assez long, alors qu’en général le problème vient du dernier élément modifié
  • Excellent billet de blog

    • Il y a beaucoup de billets remarquables sur ce site, et si vous aimez apprendre la programmation fonctionnelle, je vous recommande d’y jeter un œil
    • lien
  • Technique bonus liée au conseil « déplacer vers un troisième package »

    • Lorsqu’on génère de nombreuses structures de modèles (SQL, Protobuf, GraphQL, etc.), on peut définir une direction claire entre les couches générées
    • On fournit ensuite tout le code généré au code applicatif comme un « package de base », afin de tout assembler au même endroit
    • Avant d’adopter cette technique, il y avait un problème où « les modèles importaient les modèles de façon circulaire », mais il a complètement disparu avec l’ajout de cette couche structurelle supplémentaire
  • 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

    • En réalité, c’est possible en Go avec go:linkname
  • 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

    • En résumé, il ne faut pas faire ça non plus
  • Belle explication de la façon dont Jerf pense les packages et traite les dépendances circulaires