Go (Golang) est rapidement devenu le langage de choix pour l'infrastructure cloud-native et les systèmes backend haute performance. Sa philosophie met l'accent sur la simplicité, la concurrence explicite et le minimalisme. Pour les développeurs provenant de langages comme Java, C++ ou C#, l'envie d'appliquer des modèles de conception orientés objet lourds est naturelle. Cependant, l'identité distincte de Go — privilégiant la composition à l'héritage et les interfaces aux classes — nécessite une approche nuancée de la conception architecturale. Cet article explore comment adapter les modèles de conception classiques au code Go idiomatique, garantissant que votre logiciel reste à la fois robuste et maintenable.
La voie Go : La composition plutôt que l'héritage
Le changement le plus significatif dans la façon de penser lors de l'adoption de Go consiste à s'éloigner de la relation « est-un » (héritage) au profit de la relation « a-un » (composition). Les modèles de conception traditionnels s'appuient souvent sur des hiérarchies d'héritage profondes, ce que Go décourage explicitement. Au lieu de cela, les développeurs Go exploitent les champs anonymes pour intégrer le comportement et l'intégration d'interfaces pour définir les contrats.
Par exemple, si vous avez une structure Database et une structure RedisClient, au lieu de créer une super-classe StorageEngine, vous définissez une interface. Cela découple votre code des implémentations spécifiques et vous permet d'échanger des composants dynamiquement au moment de l'exécution, une fonctionnalité qui rend Go exceptionnellement flexible pour les tests et la conception modulaire.
Implémentation du modèle Stratégie
Le modèle Stratégie définit une famille d'algorithmes, encapsule chacun d'eux et les rend interchangeables. En Go, cela est naturellement réalisé grâce aux interfaces. Ce modèle est particulièrement utile lorsque vous devez changer de comportement en fonction de conditions d'exécution, comme la sélection de différents formats de journalisation ou de passerelles de paiement.
Considérons un système de traitement des paiements. Nous pouvons définir une interface Payer et implémenter des stratégies concrètes pour la carte de crédit et PayPal.
package main
import "fmt"
// L'interface Payer définit le contrat de stratégie
type Payer interface {
Pay(amount float64) error
}
// La structure CreditCard représente une stratégie concrète
type CreditCard struct{}
func (c CreditCard) Pay(amount float64) error {
fmt.Printf("Charging $%.2f via Credit Card\n", amount)
return nil
}
// La structure PayPal représente une autre stratégie concrète
type PayPal struct{}
func (p PayPal) Pay(amount float64) error {
fmt.Printf("Charging $%.2f via PayPal\n", amount)
return nil
}
// ProcessPayment démontre l'utilisation
func ProcessPayment(p Payer, amount float64) {
p.Pay(amount)
}
func main() {
processor := CreditCard{}
ProcessPayment(processor, 99.99)
payment := PayPal{}
ProcessPayment(payment, 49.50)
}
Cette approche garde la fonction ProcessPayment indépendante du méthode de paiement sous-jacente, adhérant au principe Ouvert/Fermé. Vous pouvez ajouter de nouvelles méthodes de paiement sans modifier le code existant.
Le modèle Singleton en Go
Dans de nombreux langages orientés objet, les Singletons sont implémentés via des constructeurs privés et des méthodes statiques. Go, n'ayant pas de méthodes statiques ni de constructeurs, gère l'état global différemment. La pratique standard pour un comportement de type Singleton en Go consiste à utiliser le package sync.Once. Cela garantit que la logique d'initialisation s'exécute exactement une fois, même dans des environnements hautement concurrents, offrant une sécurité des threads sans surcharge explicite de verrouillage.
package main
import (
"fmt"
"sync"
)
type Config struct {
Host string
}
var (
instance *Config
once sync.Once
)
func GetConfig() *Config {
once.Do(func() {
instance = &Config{Host: "localhost"}
})
return instance
}
Ce modèle est crucial pour gérer les connexions à la base de données, les instances de journalisation ou les configurations à l'échelle de l'application, où la duplication est inefficace ou logiquement incorrecte.
Le modèle Usine pour la création d'objets
Les modèles Usine abstraient la logique d'instanciation, vous permettant de créer des objets sans exposer la logique de création au client. En Go, les fonctions usines sont simplement des fonctions qui retournent des interfaces ou des structures. C'est idiomatique et souvent préféré aux usines basées sur des classes.
Un exemple pratique est une usine Shape (Forme). En fonction de l'entrée de l'utilisateur, vous retournez soit un Circle (Cercle), soit un Rectangle. En retournant une interface, le code client reste propre et indépendant des types spécifiques.
type Shape interface {
Area() float64
}
func NewShape(shapeType string) Shape {
switch shapeType {
case "circle":
return &Circle{radius: 5}
case "rectangle":
return &Rectangle{width: 10, height: 20}
default:
return nil
}
}
Conclusion
Implémenter des modèles de conception en Go consiste moins à respecter rigoureusement les définitions du GoF (Gang of Four) qu'à embrasser les principes fondamentaux de Go : simplicité, interfaces et composition. En privilégiant les interfaces à l'héritage et en utilisant des primitives de concurrence comme sync.Once, vous pouvez construire des systèmes évolutifs, testables et propres. Rappelez-vous, tous les problèmes ne nécessitent pas un modèle de conception. Parfois, une simple fonction ou structure est la meilleure solution. Utilisez ces modèles comme des outils pour résoudre la complexité, et non comme une exigence pour chaque morceau de code.