Chapter 15: State Pattern
大綱
When should you use it?
The state pattern is a behavioral pattern that allows an object to change its behavior at runtime
The context: the object that has a current state and whose behavior changes.
The state protocol: defines required methods and properties.
Concrete states: conform to the state protocol, If you ever need a new behavior, you define a new concrete state.

When should you use it?
Use the state pattern to create a system that has two or more states that it changes between during its lifetime.
For example, a traffic light can be defined using a closed set of “traffic light states.” In the simplest case, it progresses from green to yellow to red to green again.
If you have a class with several switch or if-else statements, try to define it using the state pattern instead. You’ll likely create a more flexible and easier maintain system as a result.
一个对象存在多个状态,不同状态下的行为会有不同,而且状态之间可以相互转换。
如果我们通过
if else
来判断对象的状态,那么代码中会包含大量与对象状态有关的条件语句,而且在添加,删除和更改这些状态的时候回比较麻烦;而如果使用状态模式。将状态对象分散到不同的类中,则可以消除if...else
等条件选择语句。
Playground example
目標:建立一個紅綠燈的狀態
let greenYellowRed: [SolidTrafficLightState] =
[.greenLight(), .yellowLight(), .redLight()]
let trafficLight = TrafficLight(states: greenYellowRed)
PlaygroundPage.current.liveView = trafficLight
// The state pattern doesn’t actually tell you where or how to perform state changes.
// In this case, you actually have two choices: you can put state change logic within TrafficLight, or you can put this within TrafficLightState
extension TrafficLightState {
public func apply(to context: TrafficLight, after delay: TimeInterval) {
let queue = DispatchQueue.main
let dispatchTime = DispatchTime.now() + delay
queue.asyncAfter(deadline: dispatchTime) { [weak self, weak context] in
guard let self = self, let context = context else { return }
context.transition(to: self)
}
}
}
建立Context
// MARK: - Context
public class TrafficLight: UIView {
// MARK: - Instance Properties
// These layers will hold onto the green/yellow/red states as sublayers.
public private(set) var canisterLayers: [CAShapeLayer] = []
public private(set) var currentState: TrafficLightState
public private(set) var states: [TrafficLightState]
// MARK: - Object Lifecycle
@available(*, unavailable,
message: "Use init(canisterCount: frame:) instead")
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) is not supported")
}
// declare init(canisterCount:frame:) as the designated initializer and provide default values for both canisterCount and frame.
public init(canisterCount: Int = 3,
frame: CGRect =
CGRect(x: 0, y: 0, width: 160, height: 420),
states: [TrafficLightState]) {
guard !states.isEmpty else {
fatalError("states should not be empty")
}
self.currentState = states.first!
self.states = states
super.init(frame: frame)
backgroundColor =
UIColor(red: 0.86, green: 0.64, blue: 0.25, alpha: 1)
createCanisterLayers(count: canisterCount)
transition(to: currentState)
}
private func createCanisterLayers(count: Int) {
let paddingPercentage: CGFloat = 0.2
let yTotalPadding = paddingPercentage * bounds.height
let yPadding = yTotalPadding / CGFloat(count + 1)
let canisterHeight = (bounds.height - yTotalPadding) / CGFloat(count)
let xPadding = (bounds.width - canisterHeight) / 2.0
var canisterFrame = CGRect(x: xPadding,
y: yPadding,
width: canisterHeight,
height: canisterHeight)
for _ in 0 ..< count {
let canisterShape = CAShapeLayer()
canisterShape.path = UIBezierPath(ovalIn: canisterFrame).cgPath
canisterShape.fillColor = UIColor.black.cgColor
layer.addSublayer(canisterShape)
canisterLayers.append(canisterShape)
canisterFrame.origin.y += (canisterFrame.height + yPadding)
}
}
public func transition(to state: TrafficLightState) {
removeCanisterSublayers()
currentState = state
currentState.apply(to: self)
nextState.apply(to: self, after: currentState.delay)
}
private func removeCanisterSublayers() {
canisterLayers.forEach {
$0.sublayers?.forEach {
$0.removeFromSuperlayer()
}
}
}
public var nextState: TrafficLightState {
guard let index = states.index(where: { $0 === currentState }),
index + 1 < states.count else {
return states.first!
}
return states[index + 1]
}
}
What should you be careful about?
Be careful about creating tight coupling between the context and concrete states.
Will you ever want to reuse the states in a different context? If so, consider putting a protocol between the concrete states and context, instead of having concrete states call methods on a specific context.
If you choose to implement state change logic within the states themselves, be careful about tight coupling from one state to the next.
Will you ever want to transition from state to another state instead? In this case, consider passing in the next state via an initializer or property.
Tutorial project
Key points
The state pattern permits an object to change its behavior at runtime. It involves three types: the context, state protocol and concrete states.
The context is the object that has a current state; the state protocol defines required methods and properties; and the concrete states implement the state protocol and actual behavior that changes at runtime.
The state pattern doesn’t actually tell you where to put transition logic between states. Rather, this is left for you to decide: you can put this logic either within the context or within the concrete states.
Last updated
Was this helpful?