Strategy(ストラテジ)は、相互交換可能なアルゴリズムのファミリーを定義し、それぞれを個別のクラス(Strategy)に配置する動作デザインパターンです。このパターンにより、実行時にアルゴリズムを選択できます。クライアントコードは共通のStrategyインターフェースを介して動作し、具体的な実装はランタイムに差し替えられます。iOSではProtocol + ストラテジクラスを介して実装され、AndroidではInterface + 実装を介して実装されます。Strategyは23のGoFパターンの1つであり、支払い処理、バリデーション、ソート、データフィルタリングに広く使用されています。詳細については、GoFの元の説明を参照してください。
重要なポイント
Strategyは23のGoF(Gang of Four)パターンの1つで、『Design Patterns: Elements of Reusable Object-Oriented Software』(1994年)で説明されています。このパターンは、実行時にアルゴリズムを選択する問題を解決します。多数の条件文(if-else、switch)を持つ単一のクラスを書く代わりに、Strategyは各アルゴリズムを共通のインターフェースを持つ個別のクラスに抽出することを提案します。コンテキスト(ストラテジを使用するクラス)はStrategyインターフェースへの参照を保持し、実行を具象ストラテジに委譲します。
パターンの構造は3つの要素で構成されます:Context(コンテキスト)はStrategyへの参照を保持し、そのメソッドを呼び出します;Strategy(インターフェース)はすべてのアルゴリズムに共通のメソッドを宣言します;ConcreteStrategy(具象ストラテジ)はインターフェースを実装し、具体的なアルゴリズムを含みます。クライアントは目的のストラテジを作成し、コンストラクタ、セッター、またはメソッドパラメータを介してコンテキストに渡します。コンテキストはどの特定のストラテジが実行されているかを知りません — インターフェースとのみ連携します。
| コンポーネント | 役割 | 例 |
|---|---|---|
| Context | Strategyへの参照を保持 | PaymentProcessor, Sorter |
| Strategy | アルゴリズムの共通インターフェース | Protocol PaymentStrategy |
| ConcreteStrategy | アルゴリズムの具象実装 | CardPayment, PayPalPayment |
Open/Closedの原則 — Strategyの主な利点です。システムは拡張に対して開かれており(新しいストラテジを追加できる)、修正に対して閉じられています(コンテキストコードを変更する必要がありません)。パターンなしでは、新しいアルゴリズムを追加するには既存のクラスを変更する必要があり、OCPに違反し、回帰エラーのリスクが高まります。Strategyはまたクラスのサイズを削減します:switch-caseのある200行のクラスの代わりに、それぞれ20行の6つのクラスが得られます。
SwiftのStrategyはProtocol(ストラテジインターフェース)とストラテジクラスまたは構造体を介して実装されます。Swiftのプロトコルは関連型とジェネリック制約をサポートしており、ストラテジを設計する際の柔軟性を提供します。コンテキストは通常、initまたはプロパティを介してストラテジを受け入れるViewModelクラスまたはサービスです。このパターンは、iOSプロジェクトでイベント処理、アニメーション、データフォーマット、UIストラテジに広く使用されています。
// 1. Protocol Strategy
protocol PaymentStrategy {
func pay(amount: Decimal) async throws -> PaymentResult
}
// 2. Concrete Strategies
struct CardPaymentStrategy: PaymentStrategy {
let cardNumber: String
let cvv: String
func pay(amount: Decimal) async throws -> PaymentResult {
// 銀行APIへのリクエスト送信
return PaymentResult(status: .success, transactionId: "tx_\(UUID())")
}
}
struct PayPalPaymentStrategy: PaymentStrategy {
let email: String
func pay(amount: Decimal) async throws -> PaymentResult {
// PayPal SDKへのリダイレクト
return PaymentResult(status: .success, transactionId: "pp_\(UUID())")
}
}
// 3. Context
class PaymentProcessor {
private var strategy: PaymentStrategy
init(strategy: PaymentStrategy) {
self.strategy = strategy
}
func setStrategy(_: PaymentStrategy) {
strategy = strategy
}
func processPayment(amount: Decimal) async throws -> PaymentResult {
return try await strategy.pay(amount: amount)
}
}
// 使用法
let processor = PaymentProcessor(strategy: CardPaymentStrategy(cardNumber: "4111...", cvv: "123"))
let result = try await processor.processPayment(amount: 99.99)
SwiftUIでのStrategy — パターンはMVVMと自然に統合されます。ViewModelにはストラテジプロパティが含まれ、ユーザーアクションに応じてそのメソッドを呼び出します。SwiftUI Viewは@Publishedまたは@Stateを介してデータを受け取ります — ストラテジはViewから実装の詳細を隠します。例えば、テキストバリデーションストラテジ(emailValidator、phoneValidator)は入力フィールドのタイプに応じて切り替えられます。StrategyをSwiftUIと組み合わせることで、UIKitから継承せずに柔軟性が得られます。
KotlinのStrategyは言語レベルでInterfaceを使用し、簡略化のために関数型インターフェース(SAM)を使用します。Kotlinはラムダをサポートしており、個別のストラテジクラスを宣言せずに関数としてアルゴリズムを渡すことができます。Androidでは、このパターンはViewModelとUse Casesでデータロード、キャッシング、エラー処理のアルゴリズムを分離するために使用されます。Clean Architectureを使用するAndroidプロジェクトは、フラグ(mock、real、cache)に応じて異なるリポジトリ実装を注入するためにStrategyを使用します。
// 1. Interface Strategy
interface PaymentStrategy {
suspend fun pay(amount: BigDecimal): PaymentResult
}
// 2. Concrete Strategies
class CardPaymentStrategy(
private val cardNumber: String,
private val cvv: String
) : PaymentStrategy {
override suspend fun pay(amount: BigDecimal): PaymentResult {
// Retrofit経由の銀行API
return PaymentResult(success = true, transactionId = "tx_${UUID.randomUUID()}")
}
}
class PayPalPaymentStrategy(
private val email: String
) : PaymentStrategy {
override suspend fun pay(amount: BigDecimal): PaymentResult {
// PayPal SDK統合
return PaymentResult(success = true, transactionId = "pp_${UUID.randomUUID()}")
}
}
// 3. Context
class PaymentProcessor(
private val strategy: PaymentStrategy
) {
fun setStrategy(strategy: PaymentStrategy): PaymentProcessor {
return PaymentProcessor(strategy)
}
suspend fun processPayment(amount: BigDecimal): PaymentResult {
return strategy.pay(amount)
}
}
// ViewModelでの使用
class CheckoutViewModel : ViewModel() {
private var processor = PaymentProcessor(CardPaymentStrategy("4111...", "123"))
fun payWithCard() {
viewModelScope.launch {
val result = processor.processPayment(BigDecimal("99.99"))
// 結果の処理
}
}
}
Hilt/DaggerとのStrategy — Androidプロジェクトでは、ストラテジはしばしばDIを介して注入されます。Hiltは@Bindsまたは@Providesを介してPaymentStrategyの具象実装を提供します。これにより、コンテキストコードを変更せずにストラテジを変更できます — 別のビルド(debug/release)のDIモジュールを変更するだけです。例えば、デバッグ用にMockPaymentStrategyが注入され、本番用に実際の銀行ストラテジが注入されます。Strategy + DIの組み合わせは最大の柔軟性を提供します。
Strategy vs State — 構造的にパターンは同一です:両方ともインターフェースと具象クラスとのコンポジションを使用します。違いは目的にあります:Strategyは独立したアルゴリズムを選択し、Stateはその状態に応じてオブジェクトの動作を制御します。Stateでは、コンテキスト自体が状態の変化に応じてストラテジを変更します;Strategyでは、コンテキストは切り替えを制御しません — クライアントが明示的にアルゴリズムを設定します。ストラテジは互いに認識しませんが、状態は互いに遷移できます。
Strategy vs Command — Commandは単一のアクションをオブジェクトとしてカプセル化し、Strategyは交換可能なアルゴリズムのセットをカプセル化します。Commandは「何をするか」(単一のexecute呼び出し)、Strategyは「どのようにするか」(複数ステップのアルゴリズム)です。Commandはキュー、遅延実行、元に戻す/やり直しに使用されます。Strategyは実行時にタスクを実行する方法を選択するために使用されます。コマンドはストラテジでパラメータ化でき、両方のパターンを組み合わせることができます。
| 特性 | Strategy | State | Command | Template Method |
|---|---|---|---|---|
| 目的 | 交換可能なアルゴリズム | 状態による動作 | リクエストのカプセル化 | アルゴリズムの骨格 |
| 切り替え | クライアントが明示的 | コンテキストが自動的 | クライアントまたはキュー | 継承による |
| レベル | オブジェクト(コンポジション) | オブジェクト(コンポジション) | オブジェクト | クラス(継承) |
Strategy vs Template Method — 両方のパターンはアルゴリズムを定義しますが、方法が異なります。Template Methodは継承を使用します:基本クラスがアルゴリズムの骨格(テンプレートメソッド)を定義し、サブクラスが個々のステップをオーバーライドします。Strategyはコンポジションを使用します:アルゴリズムは完全に別のクラスに外部化されます。Template Methodは固定のアルゴリズム構造の場合にシンプルで、Strategyはアルゴリズムが完全に異なり動的に変更できる場合に適しています。
支払い処理 — Strategyの古典的な例です。オンラインストアのショッピングカートにはアイテムのリストが含まれ、支払い方法はユーザーが選択します。各方法(カード、PayPal、Apple Pay、Google Pay、暗号通貨)は共通のpay(amount)シグネチャを持つ個別のストラテジです。PaymentProcessorコンテキストは支払いがどのように処理されるかを知りません — 共通のメソッドを呼び出すだけです。新しい支払い方法を追加しても、カートコードを変更する必要はありません。
データバリデーション — Strategyは同じフィールドの異なるバリデーションルールに使用されます。EmailValidatorStrategy、PhoneValidatorStrategy、AgeValidatorStrategyはvalidate(input)メソッドを持つ共通のValidationStrategyインターフェースを実装します。登録フォームは各フィールドをチェックするためにストラテジのセットを使用します。バリデーションストラテジはチェーン(Chain of Responsibility)で結合したり、ループで一度にすべて適用したりできます。これにより、長いif-elseチェックがポリモーフィックなバリデーターのコレクションに置き換えられます。
// ソートストラテジ
protocol SortingStrategy {
func sort<T>(_ items: [T]) -> [T] where T: Comparable
}
struct QuickSortStrategy: SortingStrategy {
func sort<T>(_ items: [T]) -> [T] { /* quicksort */ items }
}
struct MergeSortStrategy: SortingStrategy {
func sort<T>(_ items: [T]) -> [T] { /* mergesort */ items }
}
class SortedDataSource<T> {
private var strategy: SortingStrategy
func display(_ items: [T]) { let sorted = strategy.sort(items) }
}
認証 — モバイルアプリでは、プロバイダーに応じて認証ストラテジが切り替えられます。AuthStrategyはlogin()、logout()、getToken()メソッドを持ち、EmailPasswordAuth、GoogleAuth、AppleAuth、BiometricAuth用に実装されます。AuthManagerコンテキストはDIまたはファクトリを介してストラテジを受け入れます。これにより、ログイン画面を変更せずに新しい認証プロバイダーを追加できます。Strategyパターンは多くのOAuthライブラリとFirebase Authenticationの基盤です。
よくある質問
Strategyは、変更または拡張される可能性のある3つ以上のアルゴリズムがある場合に適しています。2つのアルゴリズムがあり安定している場合は、単純なif-elseの方がコストが低くなります。アルゴリズムがアプリケーションの異なる部分で使用される場合、実行時にアルゴリズムを切り替える必要がある場合、または各アルゴリズムが独自の依存関係とテストを必要とする場合にStrategyを使用してください。
いいえ、これらは似た構造を持つ異なるパターンです。Strategy — クライアントが明示的にアルゴリズムを選択し、ストラテジは独立しています。State — 内部状態が変化するとオブジェクト自体が動作を変更し、状態は互いに遷移できます。Stateではコンテキストが状態変更を管理し、Strategyではクライアントコードが管理します。
はい、SwiftとKotlinではストラテジをクロージャまたはラムダとして渡せます。Swift:typealias PaymentHandler = (Decimal) async throws -> PaymentResult。Kotlin:typealias PaymentFun = suspend (BigDecimal) -> PaymentResult。これにより単純なケースではコードが簡素化されますが、命名とドキュメントが失われます。1〜2のアルゴリズムにはクロージャで十分ですが、4つ以上には個別のクラスの方が適しています。
各ストラテジはモック依存関係を使用して個別の単体テストでテストされます。コンテキストはモックストラテジでテストされ、コンテキストがストラテジのメソッドを呼び出し、正しいパラメータを渡すことを検証します。SwiftではXCTest + プロトコルをモックに使用し、KotlinではMockKまたはMockitoを使用します。主な利点:各ストラテジは複雑なセットアップなしで分離してテストされます。
はい、Strategyは『Design Patterns: Elements of Reusable Object-Oriented Software』(Gamma、Helm、Johnson、Vlissides、1994年)で説明されている23のパターンの1つです。動作パターンのグループに属します。別名:Policy。Smalltalk-80での元のサンプルコードはGoFの原著で入手できます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。