DecoratorとしてのMiddleware
CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 97/107。
ミドルウェアは、Goにおけるデコレーターパターンの最も実用的な応用例の一つです。ウェブ開発において、ミドルウェア関数はHTTPハンドラーをラップし、元のハンドラーを変更することなく、ロギング、認証、タイミングなどの横断的関心事を追加します。
重要な洞察は、ミドルウェアがハンドラを受け取り、振る舞いが追加された新しいハンドラを返すということです。これはまさにデコレータパターンに従っています。つまり、入口と出口で同じインターフェースを持ち、機能が強化されています:
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)
}
}各ミドルウェアは、チェーン内の次のハンドラーをラップし、ラップされたハンドラーを呼び出す前または後に自身の動作を追加します。これらを合成することで、複数のミドルウェアをスタックできます:
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// [LOG] [AUTH] Response to: GET /usersラッピングの順序によって実行順序が決まります。つまり、最も外側のミドルウェアが最初に実行されます。このパターンは、Goのウェブフレームワークのリクエスト処理パイプラインで広く使用されており、コアとなるハンドラーのロジックに手を加えることなく、機能を簡単に追加または削除できるようになります。
チャレンジ
簡単デコレータとしてのミドルウェア(Middleware as Decorator)パターンを使用して、リクエスト処理パイプラインを構築しましょう!コアとなるハンドラーのロジックを変更することなく、タイミング、バリデーション、フォーマットなどの機能を追加するために、ミドルウェア関数がハンドラーをラップするシステムを作成します。
コードは3つのファイルに分けて構成します:
handler.go: ハンドラー型と、リクエストを処理するベースハンドラーを定義します。リクエスト文字列を受け取り、レスポンス文字列を返す関数として
Handler型を作成します:type Handler func(request string) string受け取ったリクエストに対して
Processed: [request]を返すBaseHandler関数を実装します。middleware.go: ハンドラーに追加の動作をラップするミドルウェア関数を作成します。Handlerを受け取り、新しいHandlerを返す3つのミドルウェア関数を構築します:TimingMiddleware— レスポンスをタイミング情報でラップし、[TIMING] [response]を返します。ValidationMiddleware— バリデーションのプレフィックスを追加し、[VALID] [response]を返します。UppercaseMiddleware— レスポンス全体を大文字に変換します。
各ミドルウェアは、ラップされたハンドラーを呼び出し、その結果を強化する必要があります。
main.go: ミドルウェアを合成して処理パイプラインを構築します。適用するミドルウェアレイヤーの数を読み取ります。次に、各レイヤーについて、ミドルウェアのタイプ(
timing、validation、またはuppercase)を読み取り、現在のハンドラーをそのミドルウェアでラップします。読み取った順序で適用してください。最初に読み取ったミドルウェアが最も外側のラッパーになります。パイプラインを構築した後、リクエスト文字列を読み取り、合成したハンドラーで処理して、結果を出力します。
以下の入力が提供されます:
- 1行目:ミドルウェアレイヤーの数
- 続く行:1行につき1つのミドルウェアタイプ(
timing、validation、またはuppercase) - 最後の行:処理するリクエスト
例えば、以下の場合:
2
timing
validation
GET /users出力は以下のようになります:
[TIMING] [VALID] Processed: GET /usersまた、以下の場合:
3
uppercase
timing
validation
POST /data出力は以下のようになります:
[TIMING] [VALID] PROCESSED: POST /DATAまた、以下の場合:
1
uppercase
hello world出力は以下のようになります:
PROCESSED: HELLO WORLDまた、以下の場合:
0
simple request出力は以下のようになります:
Processed: simple requestミドルウェアのラッピング順序が出力にどのように影響するかに注目してください。最初に適用された(最も外側の)ミドルウェアが最初に実行され、すべての内側のミドルウェアからの最終結果を受け取ります。2番目の例の uppercase ミドルウェアは最後に実行される(最も内側)ため、timing と validation がプレフィックスを追加する前に、ベースレスポンスを大文字に変換します!
チートシート
Goにおけるミドルウェアは、元のハンドラーを変更することなく機能を追加するためにハンドラーをラップすることで、Decoratorパターンに従います。
ハンドラー型を関数として定義します:
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)
}
}複数のミドルウェアを合成することでスタックします:
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// 出力: [LOG] [AUTH] Response to: GET /users最も外側のミドルウェアが最初に実行され、実行順序が決定されます。各ミドルウェアは、ラップされたハンドラーを呼び出す前または後に動作を追加できます。
自分で試してみよう
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// ミドルウェア層の数を読み込む
nLine, _ := reader.ReadString('\n')
n, _ := strconv.Atoi(strings.TrimSpace(nLine))
// ミドルウェアのタイプを読み込む
middlewareTypes := make([]string, n)
for i := 0; i < n; i++ {
line, _ := reader.ReadString('\n')
middlewareTypes[i] = strings.TrimSpace(line)
}
// リクエストを読み込む
request, _ := reader.ReadString('\n')
request = strings.TrimSpace(request)
// TODO: BaseHandler から開始する
// TODO: ハンドラーをミドルウェアでラップしてパイプラインを構築する
// ミドルウェアを順番に適用する - 最初に読み込まれたミドルウェアが最も外側のラッパーになる
// ヒント: これを実現するには、middlewareTypes を逆順にループする
// 最後に適用されたミドルウェアが最も外側になり、最初に実行される
// TODO: 構成されたハンドラーを通じてリクエストを処理し、結果を出力する
fmt.Println("TODO: implement pipeline")
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー11標準ライブラリとOOP
io.Reader と io.Writersort.Interfacefmt.Stringer インターフェースencoding/json と構造体http.Handler インターフェースまとめ:REST API モデル14デザインパターン パート2
CommandパターンAdapterパターンDecoratorパターンTemplate MethodパターンStateパターンCompositeパターンDecoratorとしてのMiddleware