Stan to behawioralny wzorzec projektowy pozwalający zmieniać zachowanie obiektu w odpowiedzi na zmianę jego wewnętrznego stanu.
Wzorzec Stan zakłada ekstrakcję zachowań odnoszących się do stanu obiektu do osobnych klas odpowiadających jego poszczególnym stanom. Pierwotny obiekt wówczas deleguje pracę instancjom tych klas, zamiast wykonywać ją samodzielnie.
Przykład koncepcyjny
Poniższy przykład ilustruje strukturę wzorca Stan ze szczególnym naciskiem na następujące kwestie:
Z jakich składa się klas?
Jakie role pełnią te klasy?
W jaki sposób elementy wzorca są ze sobą powiązane?
Poznawszy strukturę wzorca będzie ci łatwiej zrozumieć następujący przykład, oparty na prawdziwym przypadku użycia Swift.
Example.swift: Przykład koncepcyjny
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: class {
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: Wynik działania
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
Przykład z prawdziwego życia
Example.swift: Przykład z prawdziwego życia
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: Wynik działania
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...