architecture hexagonale Go

Architecture hexagonale Go : le guide maître des ports et adapters

Tutoriel Go

Architecture hexagonale Go : le guide maître des ports et adapters

L’architecture hexagonale Go est un pattern de conception logicielle révolutionnaire qui permet de séparer la logique métier des détails d’implémentation technique. Ce concept, également connu sous le nom de pattern ‘Ports et Adapters’, vise à isoler le cœur de votre application (le domaine) des éléments extérieurs tels que les bases de données, les API HTTP ou les services de messagerie. En adoptant cette approche, les développeurs Go peuvent créer des systèmes hautement testables, maintenables et surtout, agnostiques vis-à-vis des technologies de périphérie.

Dans un monde de microservices en constante évolution, la maîtrise de l’architecture hexagonale Go est devenue un atout majeur pour les ingénieurs logiciel. Le contexte actuel de développement cloud-native exige une capacité de changement rapide : passer d’une base de données SQL à une base NoSQL, ou d’un serveur REST à un protocole gRPC, ne devrait pas nécessiter une réécriture complète de votre logique métier. L’utilisation de l’architecture hexagonale Go permet justement cette flexibilité en utilisant l’inversion de dépendance.

Dans cet article, nous allons explorer en profondeur les fondements de ce pattern. Nous commencerons par une analyse théorique de la séparation entre le domaine et l’infrastructure. Ensuite, nous passerons à la pratique avec une implémentation concrète en Go, illustrant la création de ports (interfaces) et d’adapters (implémentations). Nous détaillerons ensuite l’explication du code pour bien comprendre le flux de données. Enfin, nous aborderons des cas d’usage avancés, les erreurs classiques à éviter et les meilleures pratiques professionnelles pour garantir la pérennité de vos projets Go.

architecture hexagonale Go
architecture hexagonale Go — illustration

🛠️ Prérequis

Avant de plonger dans l’implémentation de l’architecture hexagonale Go, assurez-vous de posséder les bases suivantes :

  • Maîtrise du langage Go : Compréhension approfondie des interfaces, des structures et de la gestion des erreurs. Version recommandée : Go 1.21 ou supérieure pour profiter des dernières optimisations de generics.
  • Environnement de développement : Un terminal fonctionnel et un éditeur de code comme VS Code ou GoLand configuré avec l’extension Go officielle.
  • Outils de gestion : Connaissance de go mod pour la gestion des dépendances. Installation via wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz.
  • Concepts de design : Notions de base sur l’inversion de dépendance (DIP) et le principe de responsabilité unique (SRP).

📚 Comprendre architecture hexagonale Go

Comprendre l’architecture hexagonale Go

L’architecture hexagonale, contrairement à ce que son nom suggère, ne définit pas un nombre de côtés, mais symbolise une frontière hermétique autour du domaine métier. Imaginez une prise électrique murale (le Port) et un adaptateur de voyage (l’Adapter). La prise est une interface standardisée : peu importe que vous branchiez une lampe, un chargeur ou un ventilateur, tant que la fiche respecte la forme de la prise, l’énergie circule. Dans notre code, le Port est une interface Go, et l’Adapter est la structure qui implémente cette interface.

Le concept repose sur une distinction stricte entre deux zones :

  • Le Domaine (Le Cœur) : Contient les entités métier, les règles de gestion et les interfaces (Ports). Il est totalement ignorant de l’existence de la base de données ou du web.
  • L’Infrastructure (La Périphérie) : Contient les implémentations techniques (Adapters) comme les clients SQL, les contrôleurs HTTP, ou les clients Kafka.

Voici une représentation schématique en ASCII :

      [ Adapter REST ] ----> [ Port (Interface) ] $
                                    | $
      [ Domain Logic  ] <-----------| $
                                    | $
      [ Adapter SQL  ] <--- [ Port (Interface) ]

Comparé à une architecture en couches (Layered Architecture) classique, où la couche service dépend directement de la couche DAO (Data Access Object), l’architecture hexagonale Go inverse cette dépendance. En utilisant l’inversion de dépendance, le domaine définit ce dont il a besoin, et l’infrastructure fournit ce besoin. Cette approche est similaire au pattern Clean Architecture de Robert C. Martin, mais elle est particulièrement élégante en Go grâce à la puissance de ses interfaces implicites.

architecture hexagonale Go
architecture hexagonale Go

🐹 Le code — architecture hexagonale Go

Go
package main

import (
	"errors"
	"fmt"
)

// --- DOMAIN LAYER ---

// User est notre entité métier fondamentale.
type User struct {
	ID    string
	Email string
}

// UserRepository est un Port (Interface) défini par le domaine.
// Le domaine décide de ce qu'il attend de la persistance.
type UserRepository interface {
	Save(user User) error
	GetByID(id string) (User, error)
}

// UserService contient la logique métier pure.
type UserService struct {
	repo UserRepository
}

// NewUserService est un constructeur injectant le port.
func NewUserService(r UserRepository) *UserService {
	return &UserService{repo: r}
}

// Register execute une règle métier : un utilisateur doit avoir un email valide.
func (s *UserService) Register(id, email string) error {
	if email == "" {
		return errors.New("email is required")
	}
	user := User{ID: id, Email: email}
	return s.repo.Save(user)
}

// --- ADAPTER LAYER (In-Memory Implementation) ---

// InMemoryUserAdapter est un Adapter qui implémente le port UserRepository.
type InMemoryUserAdapter struct {
	storage map[string]User
}

func NewInMemoryAdapter() *InMemoryUserAdapter {
	return &InMemoryUserAdapter{storage: make(map[string]User)}
}

func (a *InMemoryUserAdapter) Save(u User) error {
	a.storage[u.ID] = u
	return nil
}

func (a *InMemoryUserAdapter) GetByID(id string) (User, error) {
	user, ok := a.storage[id]
	if !ok {
		return User{}, errors.New("user not found")
	}
	return user, nil
}

// --- MAIN (Dependency Injection) ---

func main() {
	// Initialisation de l'adapter (Infrastructure)
	adapter := NewInMemoryAdapter()

	// Injection de l'adapter dans le service (Domain)
	service := NewUserService(adapter)

	// Utilisation du service
	err := service.Register("123", "expert@golang.org")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	user, _ := adapter.GetByID("123")
	fmt.Printf("User registered: %+v\n", user)
}

📖 Explication détaillée

Le premier snippet présenté illustre parfaitement le déploiement de l’architecture hexagonale Go. Décortiquons ensemble les composants clés de cette implémentation.

Analyse de la structure du domaine

Le bloc de code commence par la couche Domain. Vous y trouverez l’entité User et, surtout, l’interface UserRepository. C’est ici que réside la magie de l’inversion de dépendance. Notez que le domaine ne définit pas comment sauvegarder un utilisateur, il définit seulement la signature de la méthode nécessaire. En faisant cela, le domaine devient totalement indépendant de toute technologie externe. Si nous utilisions une classe ou une structure rigide sans interface, nous serions prisonniers de l’implémentation.

Le rôle du service et de l’injection

Le UserService agit comme le chef d’orchestre. Il ne connaît que le UserRepository (le Port). Lors de la création via NewUserService, nous pratiquons l’injection de dépendance. Ce choix technique est crucial : il nous permet de passer un InMemoryUserAdapter lors des tests unitaires, et un PostgresUserRepository en production, sans changer une seule ligne de la logique métier de Register.

  • La méthode Register : Elle contient la logique de validation. Si l’email est vide, elle retourne une erreur immédiatement. C’est la règle métier pure.
  • L’Adapter InMemory : C’est une implémentation de test. Elle utilise une simple map pour simuler une base de données. C’est extrêmement rapide et ne nécessite aucun service externe.

Un piège potentiel est de laisser fuiter des types de la couche infrastructure (comme des tags SQL) dans la couche domaine. Gardez vos entités de domaine « propres » et utilisez des DTO (Data Transfer Objects) si nécessaire pour les adapters.

🔄 Second exemple — architecture hexagonale Go

Go
package main

import (
	"database/sql"
	"fmt"
)

// PostgresUserRepository est un exemple d'adapter avancé utilisant SQL.
type PostgresUserRepository struct {
	db *sql.DB
}

// Save implémente le port avec une logique de persistance réelle.
func (r *PostgresUserRepository) Save(u User) error {
	query := `INSERT INTO users (id, email) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET email = $2`
	_, err := r.db.Exec(query, u.ID, u.Email)
	if err != joyn {
		return fmt.Errorf("failed to save user to postgres: %w", err)
	}
	return nil
}

// GetByID récupère l'utilisateur depuis la base de données.
func (r *PostgresUserRepository) GetByID(id string) (User, error) {
	user := User{}
	query := `SELECT id, email FROM users WHERE id = $1`
	err := r.db.QueryRow(query, id).Scan(&user.ID, &user.Email)
	if err != nil {
		return User{}, err
	}
	return user, nil
}

▶️ Exemple d’utilisation

Pour tester notre implémentation, nous utilisons le fichier main.go fourni. Le scénario est simple : nous instancions un adaptateur mémoire, nous l’injectons dans notre service, nous enregistrons un utilisateur, puis nous vérifions sa présence. Voici la commande pour exécuter le code :

go run main.go

La sortie console attendue est la suivante :

User registered: {ID:123 Email:expert@golang.org}

Chaque partie de la sortie confirme le succès du flux : l’ID et l’email correspondent exactement aux données injectées dans le service, prouvant que le passage entre le service (domaine) et l’adaptateur (infrastructure) a fonction"t sans perte de données ni erreur de mapping.

🚀 Cas d’usage avancés

L’application de l’architecture hexagonale Go s’étend bien au-delà d’une simple base de données. Voici trois scénarios réels où ce pattern excelle :

1. Multi-protocole avec gRPC et REST

Dans un environnement microservices, vous pourriez avoir besoin d’exposer votre service via une API REST pour le web et via gRPC pour la communication inter-services. Grâce aux ports, vous créez deux adapters différents (RestAdapter et GRPCAdapter) qui appellent tous deux le même UserService. Le domaine reste inchangé, seul le point d’entrée varie. service.Register(id, email) est appelé de la même manière par les deux contrôleurs.

2. Persistance Polyglotte

Imaginez un projet qui commence avec MongoDB mais qui doit migrer vers PostgreSQL pour des raisons de transactions ACID. Avec l’architecture hexagonale, vous développez un nouveau PostgresUserRepository. Tant que l’interface UserRepository est respectée, votre application ne verra même pas la différence lors du changement de driver. Cela réduit drendeusement le risque de régression logicielle lors de migrations critiques.

3. Intégration d’Événements (Event-Driven)

Un autre cas d’usage avancé est l’utilisation d’un port de sortie (Output Port) pour la messagerie. Votre domaine peut définir un UserEventPublisher. Un adapter peut implémenter ce port en envoyant des messages vers Apache Kafka, tandis qu’un autre pourrait simplement les logger dans la console pour le debugging. publisher.Publish(userCreatedEvent) permet de découpler la logique de notification de la complexité de l’infrastructure de streaming.

⚠️ Erreurs courantes à éviter

Même les développeurs expérimentés peuvent commettre des erreurs lors de la mise en œuvre de l’architecture hexagonale Go. Voici les plus fréquentes :

  • Fuite d’implémentation (Leaking Abstractions) : Utiliser des types spécifiques à SQL (comme sql.NullString) dans vos entités de domaine. Le domaine doit rester pur.
  • Logique métier dans les Adapters : Mettre des calculs ou des validations importantes dans l’adapter SQL. L’adapter ne doit faire que de la traduction de format.
  • Trop de ports : Créer un port pour chaque petite action. Cela peut mener à une explosion de l’interfaçage et rendre le code difficile à naviguer.
  • Ignorer l’inversion de dépendance : Créer l’instance de l’adapter directement à l’intérieur du service au lieu de l’injecter via le constructeur.
📌 Points clés à retenir

  • L'architecture hexagonale isole le domaine métier des détails techniques.
  • Les Ports sont des interfaces Go définissant les besoins du domaine.
  • Les Adapters sont des implémentations techniques (SQL, REST, etc.) qui satisfont les ports.
  • Le domaine ne doit jamais dépendre de la couche infrastructure.
  • L'injection de dépendances est le moteur de ce pattern.
  • Ce pattern facilite la migration entre différentes technologies de stockage.
  • La pureté du domaine garantit une maintenance logicielle à long terme.
  • L'architecture hexagonale Go est idéale pour les architectures microservices.

✅ Conclusion

En conclusion, l’architecture hexagonale Go est bien plus qu’une simple mode de conception ; c’est une stratégie robuste pour gérer la complexité croissante des systèmes distribués. En maîtrisant les concepts de ports et d’adapters, vous vous donnez les moyens de construire des applications capables de survivre aux changements technologiques et aux évolutions de vos besoins métier. Nous avons vu comment isoler le cœur de votre application, comment utiliser les interfaces pour l’inversion de dépendance et comment créer des adaptateurs interchangeables.

Je vous encourage vivement à pratiquer ce pattern sur vos prochains projets, même pour de petites applications, afin d’en ressentir les bénéfices en matière de testabilité. Pour approfondir, n’hésitez pas à explorer les principes de la Clean Architecture ou à lire la documentation Go officielle pour perfectionner votre usage des interfaces. Un excellent exercice consiste à transformer une application monolithique simple en une structure hexagonale. N’oubliez jamais : un code bien découplé est un code qui dure. Commencez dès aujourd’hui à refactoriser vos services !

Publications similaires

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *