Menu
Coddy logo textTech

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のウェブフレームワークのリクエスト処理パイプラインで広く使用されており、コアとなるハンドラーのロジックに手を加えることなく、機能を簡単に追加または削除できるようになります。

challenge icon

チャレンジ

簡単

デコレータとしてのミドルウェア(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: ミドルウェアを合成して処理パイプラインを構築します。

    適用するミドルウェアレイヤーの数を読み取ります。次に、各レイヤーについて、ミドルウェアのタイプ(timingvalidation、または uppercase)を読み取り、現在のハンドラーをそのミドルウェアでラップします。読み取った順序で適用してください。最初に読み取ったミドルウェアが最も外側のラッパーになります。

    パイプラインを構築した後、リクエスト文字列を読み取り、合成したハンドラーで処理して、結果を出力します。

以下の入力が提供されます:

  • 1行目:ミドルウェアレイヤーの数
  • 続く行:1行につき1つのミドルウェアタイプ(timingvalidation、または 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")
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン