ソフトウェア設計

SwiftのStrategy Pattern(戦略パターン):プロトコルとクロージャでアルゴリズムを差し替える

支払い方法ごとにif-elseが際限なく増えていくコードに悩んだことはありませんか?

読了 5 分
SwiftのStrategy Pattern(戦略パターン):プロトコルとクロージャでアルゴリズムを差し替えるのカバー画像

支払い方法ごとにif-elseが際限なく増えていくコードに悩んだことはありませんか?

ソートオプション、割引計算、フィルターロジックが1つの巨大なswitch文に絡み始めると、手を入れるのがどんどん怖くなりますよね。

そんなときに取り出したいのが、SwiftのStrategy Patternです。

今日はプロトコルとクロージャで、アルゴリズムを部品のように差し替える方法を実務の視点から解説します。

まずは要点を押さえましょう。

Strategy Patternは「何をするか」と「どうするか」を分離し、アルゴリズムを外部から差し替えられるようにする設計です。

Swiftでは、重めの方法であるプロトコルと、軽量な方法であるクロージャの2通りで実装できます。

以下で1つずつ見ていきます。


Strategy Patternとは?まずは3行で要約

難しい理論より先に、まず骨格を押さえましょう。

  1. 差し替えるアルゴリズムを1つの共通仕様(プロトコル)にまとめる
  2. 実際の動作をそれぞれの戦略オブジェクトやクロージャとして作る
  3. 利用側は仕様だけを知り、具体的な内容を知らなくてよい

ゲームキャラクターの武器を交換するようなものです。

キャラクターは「攻撃する」ことだけ知っていればよく、剣でも弓でも魔法でも、装備した武器が処理してくれます。

キャラクターのコードを変更せず、武器だけを差し替えるわけです。

この「差し替え」こそがStrategy Patternのすべてだと言ってもよいでしょう。


プロトコルで実装する(定石の方法)

最も教科書的なプロトコル方式から見ていきましょう。

割引計算を例にします。一般会員、VIP会員、クーポン適用など、計算方法が複数ある状況です。

以下は、すべての戦略が守る共通仕様と、具体的な戦略を1つ定義したコードです。

// すべての割引戦略が従う共通仕様
protocol DiscountStrategy {
    func discount(for price: Int) -> Int
}

// VIP 戦略: 20% 割引
struct VIPDiscount: DiscountStrategy {
    func discount(for price: Int) -> Int { price * 20 / 100 }
}

次に、この戦略を保持して使う側を見てみましょう。

利用側のオブジェクトは、どの戦略が渡されても気にしません。

Checkoutは仕様だけを知り、内部は知りません
Checkoutは仕様だけを知り、内部は知りません
struct Checkout {
    var strategy: DiscountStrategy   // 戦略を差し替えられる場所

    func finalPrice(_ price: Int) -> Int {
        price - strategy.discount(for: price)
    }
}

// 支払い時に戦略だけを差し替える
let cart = Checkout(strategy: VIPDiscount())

新しい割引ポリシーができても、Checkoutには手を入れません。

DiscountStrategyに準拠する構造体を1つ新しく作れば完了です。

既存コードに触れずに機能を拡張できること。これが実務で実感できる最大のメリットでした。


クロージャで実装する(Swiftらしい方法)

ただ、戦略が単純なのに毎回構造体を作るのは少し面倒ですよね。

単純なロジックまで全てプロトコルで包むと、かえってコードが冗長になりがちです。

そんなときSwiftでは、クロージャのほうがずっと軽く自然です。

struct Checkout {
    // 戦略そのものを関数として受け取る
    var discount: (Int) -> Int

    func finalPrice(_ price: Int) -> Int {
        price - discount(price)
    }
}

// 戦略をその場で定義する
let cart = Checkout(discount: { $0 * 20 / 100 })

別の型を宣言せず、渡すその場でロジックを書き込めます。

ソート基準、フィルター条件、簡単な変換など、軽量なアルゴリズムの差し替えにはこちらがずっとすっきりします。

sorted(by:)も、実は毎日使っているStrategy Patternでした
sorted(by:)も、実は毎日使っているStrategy Patternでした

実際、標準ライブラリのsorted(by:)こそ、このクロージャによるStrategy Patternです。

ソートという骨格は固定し、比較基準だけをクロージャとして差し替える構造だからです。

つまり、私たちはすでに毎日使っていたわけです。


プロトコルとクロージャ、いつどちらを使う?

私が実務で使い分けている基準を表にまとめました。

分類 プロトコル方式 クロージャ方式
適した状況 戦略が複雑・状態を保持 戦略が短く単純
再利用性 複数箇所で再利用しやすい 一度きりの用途に向く
コード量 型宣言が必要で多い その場で定義でき少ない
テスト 個別の型として検証しやすい ロジックだけを簡単に検証
可読性 名前から意図が伝わる 短ければ明快、長いと読みにくい

私の基準はこうです。

戦略が内部状態を持つ、複数画面で再利用する、または名前で意図を明確に示す必要があるなら、プロトコルが適しています。

逆に、1〜2行のロジックをその都度差し替えるなら、クロージャが正解です。

両方を組み合わせてもまったく問題ありません。私は大きなポリシーにはプロトコル、細かなオプションにはクロージャを使い分けています。


よくある質問(Q&A)

Q. Strategy Patternと継承(オーバーライド)は何が違いますか?

継承は親クラスに結び付くため、動作はコンパイル時に固定されます。

一方、Strategy Patternでは実行中に戦略を自由に差し替えられ、クラス階層にも縛られません。

Q. enumのswitch文で処理する場合とは何が違いますか?

switchでは新しいケースが増えるたびに既存コードを開いて修正する必要があります。

Strategy Patternなら新しい戦略を「追加」するだけなので、既存コードに触れる場面を減らせます。


結局、Strategy Patternの核心は1つです。変わる部分を外に出し、差し替えやすくすること。

今日学んだプロトコルとクロージャを状況に応じて使い分ければ、あのうんざりするif-else地獄からきっと抜け出せます。まずは小さなコードからリファクタリングしてみてください!

あわせて読みたい