Go Programming

Go moderne : Maîtriser les génériques avec les motifs Builder, Factory et Strategy

Avec l'introduction des génériques dans Go 1.18, le langage est passé d'un outil strictement structurel à une plateforme d'ingénierie plus expressive et sûre pour les types. Pour les développeurs intermédiaires et avancés, il ne s'agit pas seulement de commodité, mais d'éliminer le code répétitif tout en conservant la forte typage statique pour laquelle Go est célèbre. Dans cet article, nous explorerons comment implémenter trois motifs de conception fondamentaux — Builder, Factory et Strategy — en utilisant les génériques modernes de Go. Ces motifs, qui nécessitaient auparavant des interfaces complexes ou la réflexion, peuvent désormais être implémentés avec une élégance et une sécurité à la compilation.

Le motif Builder avec les génériques

Le motif Builder est essentiel pour construire des objets complexes étape par étape. Avant les génériques, les développeurs utilisaient souvent le motif « options fonctionnelles » ou s'appuyaient sur des interfaces vides, ce qui sacrifiait la sécurité des types. Avec les génériques, nous pouvons nous assurer que nos méthodes de construction retournent une structure avec un paramètre de type générique spécifique, permettant des API fluides sans perdre l'information de type. Considérons une structure de configuration pour une connexion à une base de données. En utilisant un constructeur générique, nous pouvons garantir que la sortie finale est du type correct.
package main

import "fmt"

// Structure Config générique
type Config[T any] struct {
    Driver string
    Params map[string]T
}

// Constructeur générique
type Builder[T any] struct {
    config *Config[T]
}

func NewConfigBuilder[T any]() *Builder[T] {
    return &Builder[T]{
        config: &Config[T]{
            Params: make(map[string]T),
        },
    }
}

func (b *Builder[T]) Driver(d string) *Builder[T] {
    b.config.Driver = d
    return b
}

func (b *Builder[T]) AddParam(key string, val T) *Builder[T] {
    b.config.Params[key] = val
    return b
}

func (b *Builder[T]) Build() *Config[T] {
    return b.config
}

// Utilisation
func main() {
    // T est déduit comme int
    cfg := NewConfigBuilder[int]().
        Driver("postgres").
        AddParam("max_connections", 100).
        AddParam("timeout", 30).
        Build()
    
    fmt.Println(cfg.Params["max_connections"]) // Sortie : 100
}
Cette approche garantit que `AddParam` n'accepte que le type spécifié `T`, empêchant les panic d'exécution associés à `interface{}`.

Le motif Factory : Éliminer les instructions switch

Le motif Factory abstrait la création d'objets. Dans le Go traditionnel, cela impliquait souvent de grandes instructions switch ou des cartes d'enregistrement. Les génériques nous permettent de créer une fabrique générique capable d'instancier différents types en fonction d'une fonction constructeur fournie, réduisant le couplage et améliorant la testabilité.
package main

import "fmt"

// Type de Fabrique générique
type Factory[T any] struct {
    creators map[string]func() T
}

func NewFactory[T any]() *Factory[T] {
    return &Factory[T]{
        creators: make(map[string]func() T),
    }
}

// Register permet d'ajouter des types concrets ou des fonctions de fabrique
func (f *Factory[T]) Register(name string, creator func() T) {
    f.creators[name] = creator
}

// Create instancie le type en utilisant la fonction enregistrée
func (f *Factory[T]) Create(name string) (T, error) {
    creator, ok := f.creators[name]
    if !ok {
        var t T
        return t, fmt.Errorf("type inconnu : %s", name)
    }
    return creator(), nil
}

// Exemple d'utilisation avec des structures
type Animal struct {
    Name string
}

func DogFactory() Animal {
    return Animal{Name: "Dog"}
}

func CatFactory() Animal {
    return Animal{Name: "Cat"}
}

func main() {
    animalFactory := NewFactory[Animal]()
    animalFactory.Register("dog", DogFactory)
    animalFactory.Register("cat", CatFactory)

    animal, _ := animalFactory.Create("dog")
    fmt.Println(animal.Name) // Sortie : Dog
}
Ce motif est particulièrement utile dans les architectures de plugins où les implémentations spécifiques ne sont pas connues à la compilation mais doivent respecter une interface générique ou une définition de structure.

Le motif Strategy avec les contraintes de type

Le motif Strategy définit une famille d'algorithmes, encapsule chacun d'eux et les rend interchangeables. Bien que les interfaces aient toujours été la manière Go de faire cela, les génériques offrent un avantage en termes de performance en évitant la surcharge de dispatch d'interface et en permettant un meilleur inlineage par le compilateur. Nous pouvons utiliser des contraintes de type pour garantir que notre stratégie ne s'applique qu'aux types qui prennent en charge des opérations spécifiques.
package main

import (
    "fmt"
    "golang.org/x/exp/constraints"
)

// NumericStrategy applique un algorithme à tout type numérique
type NumericStrategy[T constraints.Integer | constraints.Float] interface {
    Process(items []T) T
}

type MaxFinder[T constraints.Integer | constraints.Float] struct{}

func (m MaxFinder[T]) Process(items []T) T {
    if len(items) == 0 {
        var zero T
        return zero
    }
    max := items[0]
    for _, item := range items[1:] {
        if item > max {
            max = item
        }
    }
    return max
}

func main() {
    strategy := MaxFinder[int]{}
    result := strategy.Process([]int{1, 5, 3, 9, 2})
    fmt.Println(result) // Sortie : 9
    
    floatStrategy := MaxFinder[float64]{}
    fResult := floatStrategy.Process([]float64{1.1, 5.5, 3.3})
    fmt.Println(fResult) // Sortie : 5.5
}

Conclusion

Les génériques dans Go ne sont pas une solution miracle, mais ils constituent un outil puissant pour réduire la duplication de code et améliorer la sécurité des types. En appliquant les motifs Builder, Factory et Strategy avec les génériques, vous pouvez écrire du code qui est non seulement plus performant, mais aussi plus facile à maintenir et à tester. À mesure que l'écosystème Go mûrit, nous nous attendons à voir ces motifs devenir une pratique standard dans les applications à grande échelle. Adoptez les génériques, mais privilégiez toujours la lisibilité et la simplicité.
Share: