[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン ～Strategy/State～

この記事シリーズは、iOS/Swiftエンジニアである執筆者個人が、
**ごく普通のiOSアプリ開発でよくある状況**や
**Swiftのコアライブラリやフレームワークで使われているパターン**に
着目してデザインパターンを学び直してみた記録です。

**関連記事一覧**
[[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン](https://qiita.com/y-some/items/96db5f24ded4a1fab48f)

# Strategy/Stateパターン概要

- 条件によって違う処理内容を外部クラスに追い出し、どのクラスを使うかを実行時に選択するパターンです。
- 具体的には、**switch文の「caseが多い/caseごとの処理内容が濃い」場合に、caseごとにクラスを分離させる**ことが多いと思います。
- **「ユニットテストしやすく」「拡張がしやすい（影響範囲を限定できる）」というメリットがあります。**
- GoFのデザインパターンでは**振る舞いに関するパターン**に分類されます。
- StrategyとStateは設計としては同じで、作成側の意図が何にあるかの違いです（と私は解釈しています）。
    - Strategyは性質の違いによる振る舞いの切り替え
    - Stateは状態の変化による振る舞いの切り替え

# Strategyパターンの使い所

- switch文の caseが多い/caseごとの処理内容が濃い 場合

# Stateパターンの使い所

私なりの見解では、普通のiOSアプリでは使いどころを見つけるのはなかなか難しいと思います。

一番利用したい場面は『データの「取得開始前」「取得中」「取得成功」「取得エラー」という状態の変化でViewを更新する』ですが、UIKitがそのような設計にマッチしないためです。

# サンプルコード

Xcode 11.3 / Swift 5.1 です。
Playgroundにコピペすれば動作します。

## Strategyパターンを適用しないサンプル

```swift
// 認証パラメータ
struct AuthInfo {
    var id = ""
    var password = ""
    var token = ""

    init(id: String, password: String) {
        self.id = id
        self.password = password
    }
    init(token: String) {
        self.token = token
    }
}

// 認証管理クラス
final class AuthManager {
    enum AuthType {
        case idPassword
        case token
    }
    
    static func authenticate(by type: AuthType, with authInfo: AuthInfo) {
        switch type {
        case .idPassword:
            if authInfo.id == "id" && authInfo.password == "password" {
                print("ID Password: auth success")
            } else {
                print("ID Password: invalid id or password")
            }
        case .token:
            if authInfo.token == "token" {
                print("Token: auth success")
            } else {
                print("Token: invalid token")
            }
        }
    }
}

AuthManager.authenticate(by: .idPassword, with: AuthInfo(id: "id", password: "password"))
// ID Password: auth success

AuthManager.authenticate(by: .token, with: AuthInfo(token: "token"))
// Token: auth success
```

## Strategyパターンを適用したサンプル

```swift
// 認証パラメータ
struct AuthInfo {
    var id = ""
    var password = ""
    var token = ""

    init(id: String, password: String) {
        self.id = id
        self.password = password
    }
    init(token: String) {
        self.token = token
    }
}

// MARK: - Protocol
// 認証プロトコル
protocol AuthStrategy {
    func authenticate(_ authInfo: AuthInfo)
}

// MARK: - Context
// 認証を実行する役割
struct AuthContext {
    let strategy: AuthStrategy

    func execute(with authInfo: AuthInfo) {
        strategy.authenticate(authInfo)
    }
}

// MARK: - Concreate Strategies
// IDパスワード認証
struct IdPasswordAuthStrategy: AuthStrategy {
    func authenticate(_ authInfo: AuthInfo) {
        if authInfo.id == "id" && authInfo.password == "password" {
            print("ID Password: auth success")
        } else {
            print("ID Password: invalid id or password")
        }
    }
}
// トークン認証
struct TokenAuthStrategy: AuthStrategy {
    func authenticate(_ authInfo: AuthInfo) {
        if authInfo.token == "token" {
            print("Token: auth success")
        } else {
            print("Token: invalid token")
        }
    }
}

// MARK: - Usage
var context = AuthContext(strategy: IdPasswordAuthStrategy())
context.execute(with: AuthInfo(id: "id", password: "password"))
// ID Password: auth success

context = AuthContext(strategy: TokenAuthStrategy())
context.execute(with: AuthInfo(token: "token"))
// Token: auth success
```