この記事シリーズは、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