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


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

関連記事一覧
[iOS/Swift] アプリ開発の実務的アプローチで学ぶデザインパターン

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パターンを適用しないサンプル

// 認証パラメータ
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パターンを適用したサンプル

// 認証パラメータ
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