State を Swift で
State は、 振る舞いに関するデザインパターンの一つで、 オブジェクトの内部状態が変化した時にその振る舞いを変更することを可能とします。
このパターンは、 状態に関連した振る舞いを個別の状態のクラスへ抽出し、 元のオブジェクトが作業を自分で行わず、 これらのクラスのインスタンスに委任することを強制します。
複雑度:
人気度:
使用例: Swift では、 State パターンは、 膨大な数の switch 文に基づく状態機械をオブジェクトに変換する時によく使われます。
見つけ方: オブジェクトが、 その外的に制御される状態によって振る舞いを変えるようなメソッドを持っていたら、 State パターンを識別できます。
以下の例は Swift Playgroundsで利用できます。
Playgroundバージョンを作成してくれた Alejandro Mohamadに感謝します。
概念的な例
この例は、 State デザインパターンの構造を説明するためのものです。 以下の質問に答えることを目的としています:
- どういうクラスからできているか?
- それぞれのクラスの役割は?
- パターンの要素同士はどう関係しているのか?
ここでパターンの構造を学んだ後だと、 これに続く、 現実世界の Swift でのユースケースが理解しやすくなります。
Example.swift: 概念的な例
import XCTest /// The Context defines the interface of interest to clients. It also maintains /// a reference to an instance of a State subclass, which represents the current /// state of the Context. class Context { /// A reference to the current state of the Context. private var state: State init(_ state: State) { self.state = state transitionTo(state: state) } /// The Context allows changing the State object at runtime. func transitionTo(state: State) { print("Context: Transition to " + String(describing: state)) self.state = state self.state.update(context: self) } /// The Context delegates part of its behavior to the current State object. func request1() { state.handle1() } func request2() { state.handle2() } } /// The base State class declares methods that all Concrete State should /// implement and also provides a backreference to the Context object, /// associated with the State. This backreference can be used by States to /// transition the Context to another State. protocol State: AnyObject { func update(context: Context) func handle1() func handle2() } class BaseState: State { private(set) weak var context: Context? func update(context: Context) { self.context = context } func handle1() {} func handle2() {} } /// Concrete States implement various behaviors, associated with a state of the /// Context. class ConcreteStateA: BaseState { override func handle1() { print("ConcreteStateA handles request1.") print("ConcreteStateA wants to change the state of the context.\n") context?.transitionTo(state: ConcreteStateB()) } override func handle2() { print("ConcreteStateA handles request2.\n") } } class ConcreteStateB: BaseState { override func handle1() { print("ConcreteStateB handles request1.\n") } override func handle2() { print("ConcreteStateB handles request2.") print("ConcreteStateB wants to change the state of the context.\n") context?.transitionTo(state: ConcreteStateA()) } } /// Let's see how it all works together. class StateConceptual: XCTestCase { func test() { let context = Context(ConcreteStateA()) context.request1() context.request2() } } Output.txt: 実行結果
Context: Transition to StateConceptual.ConcreteStateA ConcreteStateA handles request1. ConcreteStateA wants to change the state of the context. Context: Transition to StateConceptual.ConcreteStateB ConcreteStateB handles request2. ConcreteStateB wants to change the state of the context. Context: Transition to StateConceptual.ConcreteStateA 現実的な例
Example.swift: 現実的な例
import XCTest class StateRealWorld: XCTestCase { func test() { print("Client: I'm starting working with a location tracker") let tracker = LocationTracker() print() tracker.startTracking() print() tracker.pauseTracking(for: 2) print() tracker.makeCheckIn() print() tracker.findMyChildren() print() tracker.stopTracking() } } class LocationTracker { /// Location tracking is enabled by default private lazy var trackingState: TrackingState = EnabledTrackingState(tracker: self) func startTracking() { trackingState.startTracking() } func stopTracking() { trackingState.stopTracking() } func pauseTracking(for time: TimeInterval) { trackingState.pauseTracking(for: time) } func makeCheckIn() { trackingState.makeCheckIn() } func findMyChildren() { trackingState.findMyChildren() } func update(state: TrackingState) { trackingState = state } } protocol TrackingState { func startTracking() func stopTracking() func pauseTracking(for time: TimeInterval) func makeCheckIn() func findMyChildren() } class EnabledTrackingState: TrackingState { private weak var tracker: LocationTracker? init(tracker: LocationTracker?) { self.tracker = tracker } func startTracking() { print("EnabledTrackingState: startTracking is invoked") print("EnabledTrackingState: tracking location....1") print("EnabledTrackingState: tracking location....2") print("EnabledTrackingState: tracking location....3") } func stopTracking() { print("EnabledTrackingState: Received 'stop tracking'") print("EnabledTrackingState: Changing state to 'disabled'...") tracker?.update(state: DisabledTrackingState(tracker: tracker)) tracker?.stopTracking() } func pauseTracking(for time: TimeInterval) { print("EnabledTrackingState: Received 'pause tracking' for \(time) seconds") print("EnabledTrackingState: Changing state to 'disabled'...") tracker?.update(state: DisabledTrackingState(tracker: tracker)) tracker?.pauseTracking(for: time) } func makeCheckIn() { print("EnabledTrackingState: performing check-in at the current location") } func findMyChildren() { print("EnabledTrackingState: searching for children...") } } class DisabledTrackingState: TrackingState { private weak var tracker: LocationTracker? init(tracker: LocationTracker?) { self.tracker = tracker } func startTracking() { print("DisabledTrackingState: Received 'start tracking'") print("DisabledTrackingState: Changing state to 'enabled'...") tracker?.update(state: EnabledTrackingState(tracker: tracker)) } func pauseTracking(for time: TimeInterval) { print("DisabledTrackingState: Pause tracking for \(time) seconds") for i in 0...Int(time) { print("DisabledTrackingState: pause...\(i)") } print("DisabledTrackingState: Time is over") print("DisabledTrackingState: Returing to 'enabled state'...\n") self.tracker?.update(state: EnabledTrackingState(tracker: self.tracker)) self.tracker?.startTracking() } func stopTracking() { print("DisabledTrackingState: Received 'stop tracking'") print("DisabledTrackingState: Do nothing...") } func makeCheckIn() { print("DisabledTrackingState: Received 'make check-in'") print("DisabledTrackingState: Changing state to 'enabled'...") tracker?.update(state: EnabledTrackingState(tracker: tracker)) tracker?.makeCheckIn() } func findMyChildren() { print("DisabledTrackingState: Received 'find my children'") print("DisabledTrackingState: Changing state to 'enabled'...") tracker?.update(state: EnabledTrackingState(tracker: tracker)) tracker?.findMyChildren() } } Output.txt: 実行結果
Client: I'm starting working with a location tracker EnabledTrackingState: startTracking is invoked EnabledTrackingState: tracking location....1 EnabledTrackingState: tracking location....2 EnabledTrackingState: tracking location....3 EnabledTrackingState: Received 'pause tracking' for 2.0 seconds EnabledTrackingState: Changing state to 'disabled'... DisabledTrackingState: Pause tracking for 2.0 seconds DisabledTrackingState: pause...0 DisabledTrackingState: pause...1 DisabledTrackingState: pause...2 DisabledTrackingState: Time is over DisabledTrackingState: Returing to 'enabled state'... EnabledTrackingState: startTracking is invoked EnabledTrackingState: tracking location....1 EnabledTrackingState: tracking location....2 EnabledTrackingState: tracking location....3 EnabledTrackingState: performing check-in at the current location EnabledTrackingState: searching for children... EnabledTrackingState: Received 'stop tracking' EnabledTrackingState: Changing state to 'disabled'... DisabledTrackingState: Received 'stop tracking' DisabledTrackingState: Do nothing...