ソフトウェア設計

Swiftミドルウェアパターン:デコレータとChain of Responsibilityの実践的な組み合わせ

ネットワークレイヤーを作っていると、必ずこんな場面に出会います。

読了 5 分
Swiftミドルウェアパターン:デコレータとChain of Responsibilityの実践的な組み合わせのカバー画像

ネットワークレイヤーを作っていると、必ずこんな場面に出会います。

1つのリクエストに認証トークンを付け、ログを残し、失敗時にはリトライまで必要になります。

すべてを1つの関数に詰め込むと、いつの間にかsend()が200行になります。

そんなときに使えるのがミドルウェアパターンです。

結論から言うと、Swiftでは各機能をデコレータパターンで包み、包んだものをChain of Responsibilityで順番に接続するのが最もすっきりします。両者は競合せず、相性のよい組み合わせです。

今回は、この組み合わせを使う理由と、コードでの組み立て方を実践例で解説します。


ミドルウェアパターンとは?

ミドルウェアは、リクエストとレスポンスの間に入る中間処理層です。

AlamofireのRequestInterceptorや、サーバー側のVaporのMiddlewareを使ったことがあれば、すでに知っている概念です。

基本となる考え方は1つです。

リクエスト処理の本体はそのままにし、その前後へ機能を自由に追加・削除できるようにします。

認証、ロギング、キャッシュ、リトライなどを、それぞれ独立した部品として作ります。

すると、今回のリクエストには認証だけ、次のリクエストには認証とキャッシュというように、自由に組み合わせられます。


デコレータとChain of Responsibilityの違いは?

この2つを混同する方は少なくありません。

簡単にまとめると、次のようになります。

区分 デコレータ Chain of Responsibility
目的 既存オブジェクトに機能を追加する 複数の処理ハンドラを並べて順番に通す
関係 包む構造(入れ子) つながる構造(チェーン)
通過するか 常に次へ渡す 途中で停止することもある
ミドルウェアでの役割 各機能を実装する 機能を接続して順序付ける

実際に使ってみると、この2つは対立ではなく、異なるレイヤーの話だと分かりました。

デコレータは「機能をどう追加するか」、Chain of Responsibilityは「追加した機能をどの順番で実行するか」です。

そのため、組み合わせることで互いの弱点を補えます。


実践的な組み合わせ:コードで組み立てる

まず、ミドルウェアの共通インターフェースを定義します。リクエストを受け取り、次のミドルウェアへ渡す構造です。

protocol Middleware {
    // requestを受け取って処理し、, nextで次の段階へ渡す
    func handle(_ request: Request,
                next: (Request) async throws -> Response)
    async throws -> Response
}

次に、各機能をデコレータのように1つずつ作ります。以下はロギングミドルウェアです。

struct LoggingMiddleware: Middleware {
    func handle(_ request: Request,
                next: (Request) async throws -> Response)
    async throws -> Response {
        print("➡️ リクエスト: \(request.url)")
        let response = try await next(request)  // 次の段階へ
        print("⬅️ レスポンス: \(response.status)")
        return response
    }
}

ここでnext(request)を呼び出す部分が、Chain of Responsibilityのつながりです。

自分の仕事(ログ出力)を済ませ、残りを次のミドルウェアへ渡します。

最後に、複数のミドルウェアを1つのチェーンにまとめます。

func buildChain(_ middlewares: [Middleware],
                final: @escaping (Request) async throws -> Response)
-> (Request) async throws -> Response {
    middlewares.reversed().reduce(final) { next, mw in
        { req in try await mw.handle(req, next: next) }
    }
}

ポイントは、reduceで後ろから包んでいくことです。

配列を[인증, 로깅, 재시도]順に入れると、リクエストはその順に通り、レスポンスは逆順に戻ります。玉ねぎの皮をイメージすると分かりやすいでしょう。

リクエストは内側へ、レスポンスは逆向きに外へ戻る構造
リクエストは内側へ、レスポンスは逆向きに外へ戻る構造
コードを書く前に、紙に層の構造を描いてみました
コードを書く前に、紙に層の構造を描いてみました

この設計のメリットは?

実際のプロジェクトに適用して感じたメリットをまとめます。

  1. 機能追加が配列に1行加えるだけになります。 キャッシュが必要なら、CachingMiddleware()を配列に入れるだけです。

  2. 順序を簡単に変えられます。 認証をロギングの前後どちらで実行するかは、配列の順序を変えるだけです。

  3. テストが簡単です。 各ミドルウェアが独立しているため、1つずつユニットテストを追加できます。

  4. 本体がすっきりします。 200行のsend()が、再び10行未満に収まります。

一方で、注意点もあります。

ミドルウェアが増えすぎると、1つのリクエストがどこを通るのか追跡しにくくなります。

そのため私は、チェーンが5、6個を超えたらロギングミドルウェアを先頭に置き、通過経路を記録します。


まとめ

デコレータで機能を作り、Chain of Responsibilityで順番に並べるこの組み合わせは、慣れればネットワークレイヤーだけでなく、イベント処理やバリデーションにもそのまま応用できます。

今日の例を小さなトイプロジェクトに組み込んでみてください。自分で層を1枚ずつ剥がしてみると、概念がずっと早く身につきます。楽しいリファクタリングを!

あわせて読みたい