#I. State Patternでお気に入りボタンの制御
##はじめに
デザインパターンを、現実に起こりそうな事例を参照しながら学習すれば、短時間で深い理解を得られる。
お気に入りボタンの制御は、現実で実装する可能性が高い機能だ。
State Patternを使ってお気に入りボタンの制御を実装することで、強固で柔軟な設計となり得る。
この記事ではState Pattern以外での実装の問題点を明らかにした後、State Patternでの実装を、主にコードを使って説明する。
##必要な前提知識
State Patternについての若干の理解(見たこと、かじったことはあるという程度)
#II. 実装するシステム
UITableViewCellにお気に入りボタンを実装する。
##ボタンには2つの状態がある。
- 登録状態
- 登録されていない状態
##ボタンが押される(Event)と、状態(State)に応じた一連の処理(Action)を行う。
-
登録状態での処理: 一連のお気に入り登録解除処理を行い、"登録されていない状態"へ移行。
-
登録されていない状態での処理: 一連のお気に入り登録処理を行い、"登録状態"へ移行。
また、登録状態ではボタンの色は赤になり、登録されていない状態ではボタンの色は元に戻る。
##状態遷移図
以下の図は状態遷移図(State Transition Diagram)と呼ばれ、お気に入りボタンの有限状態マシンを表している。
RegisteredAsFavoriteとNotRegisteredAsFavoriteの2つ状態が存在し、それぞれの状態でBtnイベントに対してAddFavoriteやRemoveFavoriteアクションが行われ、次の状態へ移行することを表現している。
#III. switch文などでの実装
上記で示したシステムをswitch文などを使って実装した場合について考えてみる。
"アジャイルソフトウェア開発の奥義"では以下のような問題が指摘されている。
"入れ子のswitch/case文の...欠点は、状態マシンの論理部分とアクション部分をうまく分離する手立てがないことだ。" (ロバート, 2004, p529)
これにより、状態マシンの状態の数が増えたり、アクションが複雑でコード量が増えた場合は、設計が非常に複雑で柔軟性のないものとなる可能性がある。
#IV. State Patternでの実装
注意: State Patternに関係のない要素を省くため、この章のコードは不完全である。UItabeleViewCellが再利用された場合の処理やその他重要な処理が記載されていない。現実に適応させるにはかなりの追加コードが必要。
再度"アジャイルソフトウェア開発の奥義"からの引用だが、State Patternを使うことで、状態マシンのアクション部分と論理部分を強力に分離することができる(ロバート, 2004, p534)。
State PatternはContext Classと一連のStateの2つの概念から構成される。
##Context Class
以下のArticleCellクラスには、現在のState(状態)および、以下の3種類の機能が実装されている。
- トリガーとなるEvent
- 状態を変化させるsetState
- Action
論理部分が含まれていないので、それらを気にせずにEventやActionを追加することができる。
import UIKit
class ArticleCell: UITableViewCell {
// 現在の状態を保持
private var state: ArticleCellState = CellStateNotRegisteredAsFavorite()
@IBOutlet weak var favoriteButton: UIButton!
@IBOutlet weak var titleLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
self.favoriteButton.backgroundColor = UIColor.clearColor()
}
// Events
@IBAction func favoriteButtonTapped() {
self.state.favoriteButtonTapped(self)
}
// State chaqnge
func setState(state: ArticleCellState) {
self.state = state
}
// Actions
func addFavorite() {
print("Adding Favorite...")
/*
Repositoryへのアクセス、サーバー上のデータの更新などの一連の処理を実装
*/
// 最後にボタンの色を変える
self.favoriteButton.backgroundColor = UIColor.redColor()
}
func removeFavorite() {
print("Removing Favorite...")
/*
Repositoryへのアクセス、サーバー上のデータの更新などの一連の処理を実装
*/
// 最後にボタンの色を変える
self.favoriteButton.backgroundColor = UIColor.clearColor()
}
}
##State Class
以下のソースファイルには実装するState(状態)のクラスが実装されている。
Protocolと複数のStateクラスを1つのファイルに実装してある。
各Stateクラスには、その状態の時にどのようなEventに対してどのようなActionを行うかが記載されている。前述のArticeCellクラスに複数のEventが存在する場合は、各Eventに対応するコードを書く必要がある。
State(状態)を追加したい場合は、EventやAction大きくに干渉されずに、追加することができる。
import UIKit
protocol ArticleCellState {
func favoriteButtonTapped(articleCell: ArticleCell)
}
// State classes
class CellStateRegisteredAsFavorite: NSObject, ArticleCellState {
func favoriteButtonTapped(articleCell: ArticleCell) {
articleCell.removeFavorite() // Do action(s) for the state
articleCell.setState( CellStateNotRegisteredAsFavorite() ) // Set new state
}
}
class CellStateNotRegisteredAsFavorite: NSObject, ArticleCellState {
func favoriteButtonTapped(articleCell: ArticleCell) {
articleCell.addFavorite() // Do action(s) for the state
articleCell.setState( CellStateRegisteredAsFavorite() ) // Set new state
}
}
#V. 結論&参考文献
上記のように実装をすれば、各Stateでのアクションを複雑にしたり、お気に入りか否か不明な状態(例えばお気に入り状態をフェッチする前など)を追加することも、比較的少ないコストで実現できる。
もちろんSwitch文などで実装したほうが単純で効率的なケースも存在するが、状態マシンが複雑な場合はState Patternは強力な実装方法の1つとなる。