Middleware jako dekorator
Część sekcji Programowanie obiektowe ścieżki GO w Coddy. Lekcja 97 z 107.
Middleware to jedno z najbardziej praktycznych zastosowań wzorca Dekorator w Go. W programowaniu internetowym funkcje middleware opakowują procedury obsługi HTTP, aby dodawać przekrojowe funkcje, takie jak rejestrowanie zdarzeń, uwierzytelnianie czy pomiar czasu, bez modyfikowania oryginalnej procedury obsługi.
Kluczowy wniosek jest taki, że oprogramowanie pośredniczące przyjmuje procedurę obsługi i zwraca nową procedurę obsługi z dodatkowym zachowaniem. Dokładnie na tym polega wzorzec Dekorator: ten sam interfejs na wejściu i wyjściu, z rozszerzoną funkcjonalnością:
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)
}
}Każde oprogramowanie pośredniczące opakowuje kolejny program obsługi w łańcuchu, dodając swoje działanie przed wywołaniem opakowanego programu obsługi lub po nim. Możesz łączyć wiele elementów oprogramowania pośredniczącego:
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// [LOG] [AUTH] Response to: GET /usersKolejność opakowywania określa kolejność wykonywania: najpierw uruchamia się najbardziej zewnętrzne oprogramowanie pośredniczące. Ten wzorzec jest szeroko stosowany w frameworkach internetowych Go do tworzenia potoków przetwarzania żądań, co ułatwia dodawanie lub usuwanie funkcjonalności bez ingerowania w logikę głównego programu obsługi.
Wyzwanie
ŁatwyZbudujmy potok przetwarzania żądań, używając wzorca Middleware jako dekoratora! Utworzysz system, w którym funkcje middleware opakowują handlery, aby dodawać funkcjonalności, takie jak pomiar czasu, walidacja i formatowanie — a wszystko to bez modyfikowania logiki głównego handlera.
Podzielisz kod na trzy pliki:
handler.go: Zdefiniuj typ handlera oraz podstawowy handler, który przetwarza żądania.Utwórz typ
Handlerjako funkcję, która przyjmuje ciąg znaków żądania i zwraca ciąg znaków odpowiedzi:type Handler func(request string) stringZaimplementuj funkcję
BaseHandler, która dla każdego otrzymanego żądania zwracaProcessed: [request].middleware.go: Utwórz funkcje middleware, które opakowują handlery, dodając dodatkowe zachowanie.Zbuduj trzy funkcje middleware, z których każda przyjmuje
Handleri zwraca nowyHandler:TimingMiddleware: opakowuje odpowiedź informacją o pomiarze czasu i zwraca[TIMING] [response]ValidationMiddleware: dodaje prefiks walidacji i zwraca[VALID] [response]UppercaseMiddleware: zamienia całą odpowiedź na wielkie litery
Każde middleware powinno wywołać opakowany handler i wzbogacić jego wynik.
main.go: Zbuduj potok przetwarzania, komponując middleware.Odczytaj liczbę warstw middleware do zastosowania. Następnie dla każdej warstwy odczytaj typ middleware (
timing,validationlubuppercase) i opakuj bieżący handler tym middleware. Zastosuj je w kolejności odczytu: pierwsze odczytane middleware staje się najbardziej zewnętrzną warstwą opakowującą.Po zbudowaniu potoku odczytaj ciąg znaków żądania, przetwórz go za pomocą utworzonego handlera i wypisz wynik.
Dane wejściowe będą miały następującą postać:
- Linia 1: Liczba warstw middleware
- Kolejne linie: Po jednym typie middleware w każdej linii (
timing,validationlubuppercase) - Ostatnia linia: Żądanie do przetworzenia
Na przykład dla danych wejściowych:
2
timing
validation
GET /usersWynik powinien być następujący:
[TIMING] [VALID] Processed: GET /usersA dla danych wejściowych:
3
uppercase
timing
validation
POST /dataWynik powinien być następujący:
[TIMING] [VALID] PROCESSED: POST /DATAA dla danych wejściowych:
1
uppercase
hello worldWynik powinien być następujący:
PROCESSED: HELLO WORLDA dla danych wejściowych:
0
simple requestWynik powinien być następujący:
Processed: simple requestZwróć uwagę, jak kolejność opakowywania przez middleware wpływa na wynik: pierwsze zastosowane middleware (najbardziej zewnętrzne) uruchamia się jako pierwsze i otrzymuje końcowy wynik wszystkich wewnętrznych middleware. W drugim przykładzie middleware zamieniające tekst na wielkie litery uruchamia się jako ostatnie (najbardziej wewnętrzne), więc zamienia odpowiedź podstawowego handlera na wielkie litery, zanim middleware pomiaru czasu i walidacji dodadzą swoje prefiksy!
Spróbuj swoich sił
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Odczytaj liczbę warstw middleware
nLine, _ := reader.ReadString('\n')
n, _ := strconv.Atoi(strings.TrimSpace(nLine))
// Odczytaj typy middleware
middlewareTypes := make([]string, n)
for i := 0; i < n; i++ {
line, _ := reader.ReadString('\n')
middlewareTypes[i] = strings.TrimSpace(line)
}
// Odczytaj żądanie
request, _ := reader.ReadString('\n')
request = strings.TrimSpace(request)
// TODO: Zacznij od BaseHandler
// TODO: Zbuduj potok, opakowując handler kolejnymi warstwami middleware
// Dodawaj middleware w odpowiedniej kolejności — pierwsza odczytana warstwa stanie się najbardziej zewnętrznym opakowaniem
// Wskazówka: Aby to osiągnąć, przejdź przez middlewareTypes w ODWROTNEJ kolejności
// Ostatnia dodana warstwa middleware będzie najbardziej zewnętrzna (uruchomi się jako pierwsza)
// TODO: Przetwórz żądanie za pomocą złożonego handlera i wypisz wynik
fmt.Println("TODO: implement pipeline")
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy OOP w Go
Pliki zewnętrzneObszar roboczy i moduły GoPakiety i importyNazwy eksportowane i nieeksportowaneWprowadzenie do OOP w GoStruktury jako klasyDefiniowanie metod dla strukturOdbiorniki wskaźnikowe i wartościoweInicjalizacja strukturFunkcje konstruktoraPodsumowanie – prosty kalkulator4Interfejsy
Wprowadzenie do interfejsówImplementacja niejawnaInterfejs jako kontraktPusty interfejs (any)Asercja typuPrzełącznik typówKompozycja interfejsówInterfejsy Stringer i ErrorPowtórka — kalkulator figur7Hermetyzacja
Pola eksportowane i nieeksportowaneHermetyzacja na poziomie pakietuMetody pobierające i ustawiająceUkrywanie informacji w GoPowtórka – dane studentów10Typy generyczne (Go 1.18+)
Wprowadzenie do typów generycznychParametry typówOgraniczenia typówStruktury generyczneObejście ograniczeń metod generycznychPodsumowanie — generyczna kolekcja2Typy i struktury w praktyce
Typy proste i złożoneDefiniowanie własnych typówTagi pól strukturStruktury anonimoweStruktury zagnieżdżoneWartości zerowe i domyślnePodsumowanie – książka kontaktów5Kompozycja zamiast dziedziczenia
Dlaczego Go nie ma dziedziczeniaPodstawy osadzania strukturPromowanie metodOsadzanie wielu strukturOsadzanie a agregacjaPrzesłanianie osadzonych metodPodsumowanie — hierarchia pracowników8Obsługa błędów i programowanie obiektowe
Interfejs błęduWłasne typy błędówOpakowywanie błędów (fmt.Errorf)Błędy wartowniczeerrors.Is() i errors.As()Panic, Defer i RecoverPodsumowanie — parser plików11Biblioteka standardowa i OOP
io.Reader i io.Writersort.InterfaceInterfejs fmt.Stringerencoding/json ze strukturamiInterfejs http.HandlerPowtórzenie – modele REST API14Wzorce projektowe, część 2
Wzorzec poleceniaWzorzec adapteraWzorzec dekoratoraWzorzec metody szablonowejWzorzec stanuWzorzec kompozytuMiddleware jako dekorator3Wskaźniki i pamięć
Podstawy wskaźników w GoWskaźniki do strukturPrzekazywanie przez wartość a przez referencjęFunkcja new()Odśmiecanie pamięci w GoPowtórka — kreator listy wiązanej6Polimorfizm w Go
Polimorfizm za pomocą interfejsówDuck typing w GoZasady spełniania interfejsówKolekcje polimorficzneWstrzykiwanie zależnościPowtórka — procesor płatności9Współbieżność i OOP
Podstawy goroutinesKanały i komunikacjaKanały buforowane i niebuforowaneInstrukcja selectsync.Mutex i sync.RWMutexsync.WaitGroupProjektowanie struktur bezpiecznych wątkowoPowtórka – pula workerów12Zaawansowane zagadnienia programowania obiektowego
Wzorzec opcji funkcyjnychWzorzec Builder w GoŁańcuchowe wywoływanie metodAliasy typów a definicje typówPodstawy refleksjiPrzegląd generowania koduPoćwicz samodzielnie: Kompilator Go online