Middleware en tant que Décorateur
Fait partie de la section Programmation Orientée Objet du Journey GO de Coddy — leçon 97 sur 107.
Le middleware est l'une des applications les plus pratiques du patron Decorator en Go. En développement web, les fonctions de middleware enveloppent les gestionnaires HTTP pour ajouter des préoccupations transversales telles que la journalisation (logging), l'authentification ou le chronométrage (timing) sans modifier le gestionnaire d'origine.
L'idée centrale est que le middleware prend un gestionnaire (handler) et renvoie un nouveau gestionnaire avec un comportement ajouté. Cela suit exactement le patron Decorator — la même interface en entrée et en sortie, avec des fonctionnalités enrichies :
type Handler func(request string) string
func LoggingMiddleware(next Handler) Handler {
return func(request string) string {
result := next(request)
return "[LOG] " + result
}
}
func AuthMiddleware(next Handler) Handler {
return func(request string) string {
return "[AUTH] " + next(request)
}
}Chaque middleware enveloppe le gestionnaire suivant dans la chaîne, en ajoutant son comportement avant ou après l'appel du gestionnaire enveloppé. Vous pouvez empiler plusieurs middlewares en les composant :
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// [LOG] [AUTH] Réponse à : GET /usersL'ordre d'imbrication détermine l'ordre d'exécution — le middleware le plus externe s'exécute en premier. Ce modèle est largement utilisé dans les frameworks web Go pour les pipelines de traitement des requêtes, ce qui permet d'ajouter ou de supprimer facilement des fonctionnalités sans toucher à la logique principale du gestionnaire (handler).
Défi
FacileConstruisons un pipeline de traitement de requêtes en utilisant le modèle Middleware as Decorator ! Vous allez créer un système où des fonctions middleware enveloppent des gestionnaires (handlers) pour ajouter des fonctionnalités telles que le chronométrage, la validation et le formatage—le tout sans modifier la logique de base du gestionnaire.
Vous organiserez votre code sur trois fichiers :
handler.go: Définissez votre type de gestionnaire et le gestionnaire de base qui traite les requêtes.Créez un type
Handlersous forme de fonction qui prend une chaîne de requête (request string) et retourne une chaîne de réponse (response string) :type Handler func(request string) stringImplémentez une fonction
BaseHandlerqui retourneProcessed: [request]pour toute requête qu'elle reçoit.middleware.go: Créez vos fonctions middleware qui enveloppent les gestionnaires avec un comportement supplémentaire.Construisez trois fonctions middleware, chacune prenant un
Handleret retournant un nouveauHandler:TimingMiddleware— enveloppe la réponse avec des informations de chronométrage, retournant[TIMING] [response]ValidationMiddleware— ajoute un préfixe de validation, retournant[VALID] [response]UppercaseMiddleware— convertit l'intégralité de la réponse en majuscules
Chaque middleware doit appeler le gestionnaire enveloppé et améliorer son résultat.
main.go: Construisez un pipeline de traitement en composant les middlewares.Lisez le nombre de couches de middleware à appliquer. Ensuite, pour chaque couche, lisez le type de middleware (
timing,validation, ouuppercase) et enveloppez votre gestionnaire actuel avec ce middleware. Appliquez-les dans l'ordre où ils sont lus—le premier middleware lu devient l'enveloppe la plus externe.Après avoir construit le pipeline, lisez la chaîne de requête, traitez-la via votre gestionnaire composé et affichez le résultat.
Les entrées suivantes seront fournies :
- Ligne 1 : Nombre de couches de middleware
- Lignes suivantes : Un type de middleware par ligne (
timing,validation, ouuppercase) - Dernière ligne : La requête à traiter
Par exemple, avec :
2
timing
validation
GET /usersVotre sortie devrait être :
[TIMING] [VALID] Processed: GET /usersEt avec :
3
uppercase
timing
validation
POST /dataVotre sortie devrait être :
[TIMING] [VALID] PROCESSED: POST /DATAEt avec :
1
uppercase
hello worldVotre sortie devrait être :
PROCESSED: HELLO WORLDEt avec :
0
simple requestVotre sortie devrait être :
Processed: simple requestRemarquez comment l'ordre d'enveloppement du middleware affecte la sortie—le premier middleware appliqué (le plus externe) s'exécute en premier et voit le résultat final de tous les middlewares internes. Le middleware uppercase dans le deuxième exemple s'exécute en dernier (le plus interne), il met donc en majuscules la réponse de base avant que le chronométrage et la validation n'ajoutent leurs préfixes !
Aide-mémoire
Les middlewares en Go suivent le motif Décorateur (Decorator pattern) en enveloppant les gestionnaires (handlers) pour ajouter des fonctionnalités sans modifier le gestionnaire d'origine.
Définissez un type de gestionnaire sous forme de fonction :
type Handler func(request string) stringLes fonctions de middleware prennent un gestionnaire et renvoient un nouveau gestionnaire avec un comportement amélioré :
func LoggingMiddleware(next Handler) Handler {
return func(request string) string {
result := next(request)
return "[LOG] " + result
}
}
func AuthMiddleware(next Handler) Handler {
return func(request string) string {
return "[AUTH] " + next(request)
}
}Empilez plusieurs middlewares en les composant :
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// Sortie : [LOG] [AUTH] Response to: GET /usersLe middleware le plus externe s'exécute en premier, déterminant l'ordre d'exécution. Chaque middleware peut ajouter un comportement avant ou après l'appel du gestionnaire enveloppé.
Essayez vous-même
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Lire le nombre de couches de middleware
nLine, _ := reader.ReadString('\n')
n, _ := strconv.Atoi(strings.TrimSpace(nLine))
// Lire les types de middleware
middlewareTypes := make([]string, n)
for i := 0; i < n; i++ {
line, _ := reader.ReadString('\n')
middlewareTypes[i] = strings.TrimSpace(line)
}
// Lire la requête
request, _ := reader.ReadString('\n')
request = strings.TrimSpace(request)
// TODO: Commencer avec le BaseHandler
// TODO: Construire le pipeline en enveloppant le handler avec le middleware
// Appliquer le middleware dans l'ordre - le premier middleware lu devient l'enveloppe la plus externe
// Hint: Parcourir middlewareTypes dans l'ordre INVERSE pour y parvenir
// Le dernier middleware appliqué sera le plus externe (s'exécute en premier)
// TODO: Traiter la requête via le handler composé et afficher le résultat
fmt.Println("TODO: implement pipeline")
}
Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.
Toutes les leçons de Programmation Orientée Objet
1Fondamentaux de la POO en Go
Fichiers externesEspace de travail et Modules GoPackages et ImportsNoms exportés vs non exportésIntroduction à la POO en GoLes Structs comme ClassesDéfinir des méthodes sur les StructsReceveurs par pointeur vs par valeurInitialisation de StructsFonctions constructeursRécapitulatif - Calculatrice simple4Interfaces
Introduction aux interfacesImplémentation impliciteL'interface comme contratInterface vide (any)Assertion de typeType SwitchComposition d'interfacesInterfaces Stringer & ErrorRécapitulatif - Calculateur de formes7Encapsulation
Champs exportés vs non exportésEncapsulation au niveau du packageMéthodes Getter & SetterMasquage d'informations en GoRécapitulatif - Dossiers étudiants10Génériques (Go 1.18+)
Introduction aux génériquesParamètres de typeContraintes de typeStructures génériquesContournement pour les méthodes génériquesRécapitulatif - Collections génériques13Patrons de conception, Partie 1
Introduction aux Design PatternsPatron SingletonPatron FactoryPatron Abstract FactoryPatron ObserverPatron Strategy2Exploration approfondie des Types et Structs
Types de base et compositesDéfinitions de types personnalisésTags de StructStructs anonymesStructs imbriquéesValeurs par défaut et Zero ValuesRécapitulatif - Carnet de contacts5Composition plutôt qu'héritage
Pourquoi Go n'a pas d'héritageLes bases de l'imbrication de structsPromotion de méthodesImbriquer plusieurs structsImbrication vs AgrégationMasquage de méthodes imbriquéesRécapitulatif - Hiérarchie des employés8Gestion des erreurs & POO
L'interface errorTypes d'erreurs personnalisésError Wrapping (fmt.Errorf)Erreurs sentinelleserrors.Is() et errors.As()Panic, Defer et RecoverRécapitulatif - File Parser11Bibliothèque standard et POO
io.Reader & io.WriterL'interface sort.InterfaceL'interface fmt.Stringerencoding/json avec les StructsL'interface http.HandlerRécapitulatif - Modèles d'API REST14Patrons de conception, Partie 2
Patron CommandePatron AdaptateurPatron DécorateurPatron Template MethodPatron ÉtatPatron CompositeMiddleware en tant que Décorateur3Pointeurs et Mémoire
Bases des pointeurs en GoPointeurs vers des structsPassage par valeur vs référenceLa fonction new()Garbage Collection en GoRécapitulatif - Constructeur de liste chaînée6Le polymorphisme en Go
Le polymorphisme via les interfacesLe Duck Typing en GoRègles de satisfaction des interfacesCollections polymorphesInjection de dépendancesRécapitulatif - Processeur de paiement9Concurrence et POO
Bases des GoroutinesChannels et CommunicationChannels avec buffer vs sans bufferL'instruction Selectsync.Mutex et sync.RWMutexsync.WaitGroupConception de structures Thread-SafeRécapitulatif - Worker Pool