Un code bien organisé est essentiel pour faciliter sa maintenance. Go, avec sa simplicité et son efficacité, offre de nombreuses ressources pour garantir une architecture bien structurée.
L’une de ces ressources est l’utilisation des interfaces, qui jouent un rôle fondamental dans la séparation des responsabilités et dans la création d’abstractions entre les couches de l’application.
Dans cet article, nous explorerons comment les interfaces peuvent être appliquées au sein d’une architecture en couches, favorisant un code découplé, testable et flexible.
Qu’est-ce qu’une architecture en couches
Une architecture en couches est un modèle de conception logiciel qui divise l’application en couches distinctes, chacune ayant une responsabilité précise.
L’idée est que chaque couche communique avec les autres de manière contrôlée, sans exposer les détails internes. Quelques exemples de ces couches incluent :
-
Couche de domaine : où résident les règles métier.
-
Couche de dépôt : responsable de la persistance des données.
-
Couche d’application : gère les flux d’opération et coordonne les actions entre les couches.
-
Ports et adaptateurs : assurent la communication entre le cœur de l’application et le monde extérieur, que ce soit avec l’utilisateur, des API ou d’autres services.
Ce schéma en couches facilite la maintenance, car chaque couche a une fonction spécifique et bien définie. De plus, il permet la mise en œuvre de tests unitaires plus isolés, puisque les dépendances entre les couches peuvent être simulées.
Qu’est-ce que les interfaces en Go
En Go, une interface est un type qui définit un ensemble de méthodes, mais ne les implémente pas directement. Ce sont les types qui les « satisfont » qui les implémentent. Par exemple :
type Repository interface {
Save(data string) error
}
Aqui, qualquer tipo que possua um método
Save(data string) error satisfaz essa interface. La flexibilité des interfaces en Go réside dans le fait qu’elles permettent de créer des abstractions, où l’on peut modifier l’implémentation réelle sans avoir à modifier le code qui utilise l’interface.Comment les utiliser pour les abstractions des couches
Bien que les exemples ci-dessous soient davantage liés au Domain-Driven Design et à l’Architecture Hexagonale, l’utilisation des interfaces pour créer de bonnes abstractions au sein des couches ne se limite pas à ces architectures.
L’objectif de cette section est de présenter des exemples sur la manière de créer des couches avec un bon découplage, les rendant plus flexibles et plus faciles à tester.
Couche de Domaine
Dans la couche de domaine, les interfaces permettent de définir des contrats décrivant les opérations métiers, mais sans se soucier des détails d’implémentation.
Supposons que nous ayons une logique métier qui doit sauvegarder une donnée, mais sans connaître le mécanisme de persistance (base de données, cache, etc.)
type DomainService struct {
repo Repository
}
func NewDomainService(repo Repository) *DomainService {
return &DomainService{repo}
}
func (s *DomainService) ProcessData(data string) error {
// Regras de negócio aqui...
return s.repo.Save(data)
}
Aici, DomainService n’a besoin que d’une interface Repository, laissant l’implémentation concrète à une autre partie de l’application.
Couche de Dépôt
La couche de dépôt contient l’implémentation concrète de l’interface utilisée dans le domaine. Cela permet, par exemple, de changer la base de données ou même d’employer un autre mécanisme de persistance (comme un fichier sur disque ou un service externe), sans modifier la logique métier.
type RepositoryDB struct {
db *sql.DB
}
func NewRepositoryDB(db *sql.DB) *RepositoryDB {
return &RepositoryDB{db}
}
func (r *RepositoryDB) Save(data string) error {
// Code pour sauvegarder la donnée dans la base de données
_, err := r.db.Exec("INSERT INTO table (dado) VALUES (?)", data)
return err
}
Comme RepositoryDB implemente la méthode Save, il peut être utilisé par la couche de domaine comme dépendance concrète de l’interface Repository.
Couche de Ports et Adaptateurs
Ports et Adaptateurs font partie de l’Architecture Hexagonale, où les interfaces sont utilisées pour définir les contrats du cœur de l’application avec le monde extérieur. Les interfaces jouent un rôle crucial en isolant la logique de l’application des détails d’entrée/sortie.
Un exemple serait un port qui définit comment l’application reçoit les données d’une API externe :
type ExternalAPI interface {
Find() (string, error)
}
type APIAdapter struct {
clientHTTP *http.Client
}
func (a *APIAdapter) Find() (string, error) {
// Implémentation pour récupérer les données d'une API externe
resp, err := a.clientHTTP.Get("https://api.external.com/data")
if err != nil {
return "", err
}
defer resp.Body.Close()
var data string
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return "", err
}
return data, nil
}
La couche d’application, quant à elle, peut utiliser cette interface, offrant une flexibilité dans l’exploitation des données :
type AppService struct {
api ExternalAPI
}
func (s *AppService) Exec() error {
data, err := s.api.Find()
if err != nil {
return err
}
// Logique de l’application avec les données récupérées
return nil
}
Conclusion
L’utilisation des interfaces en Go au sein d’une architecture en couches permet de découpler les différentes parties de l’application, facilitant le remplacement des implémentations et favorisant la testabilité.
Grâce à cette approche, il est possible de garantir que les couches de domaine, de dépôt et d’interface soient clairement séparées et suivent le principe de responsabilité unique, permettant de faire évoluer l’application de manière plus sûre et modulaire.
Les interfaces, lorsqu’elles sont utilisées correctement, rendent le code plus flexible, ce qui se traduit par une maintenance simplifiée et une plus grande capacité d’adaptation face aux changements des exigences.




