*** Updated for Swift 5.1 ***
The State pattern enables an entity, which can change its behaviour based on its internal state, to use a set of types conforming to a common interface to represent these states. This allows behaviour related to states, and transitions between states, to be modeled in a flexible and extensible manner. The State pattern provides an effective way to implement finite state machines. In this article, we work through a case study to demonstrate how the State pattern can be implemented in Swift, exploring various aspects of the pattern and showing its ability to handle changes in requirements. We also look at key design and implementation considerations as well as benefits and practical applications of the pattern.
Related article:
Contents
State pattern
1. Intent
2. Structure and participants
State machine diagram
1. States, events and transitions
2. Entry and exit activities for states
3. Transition effects and guard conditions
State Pattern implementation
1. The Context type
2. The State interface
3. The Context type revisited
4. Concrete state types
5. Test run
Dealing with Change
1. Adding a transition
2. Adding properties to concrete state types
3. Adding a transition effect
4. Adding a new state
Key design and implementation considerations
1. Handling state transitions
2. States with state
3. Creating states
4. Events and activities not applicable to certain states
5. Specifying activities in states or transitions
Benefits of the State pattern
1. Clear control flow
2. Partitioning behaviour
3. Flexibility and extensibility
Practical applications of the State pattern
1. Database updates
2. Drawing programs and frameworks
3. Graphical user interfaces
4. Network protocols
5. Game programming
6. Embedded systems
Conclusion
References
Finite state machines
A finite state machine, also called finite state automaton, is an abstract machine that can be in any one of a finite number of states, and can transition among these states in response to certain trigger events.
Real-world domains commonly contain entities that may, at any point in time, be in one of a number of possible states. Depending on what state the entity is in, it may react differently to the same action or event. These kinds of entities can be modeled effectively using finite state machines, also called finite state automata.
Consider a typical vending machine. When no money has been inserted, pressing a button that is supposed to vend a particular item has no effect. This is because the machine is not in the right state for that action to be performed. Even when money is inserted, the machine may vend some items but not others, depending on how much money has been inserted and how much each item costs. Once the item that was paid for has been given, the machine goes back to the state where it will not vend anything without receiving money.
Simple state machines can be implemented in code using conditional statements, typically nested switch statements. This may be adequate, and quite efficient, for very simple state machine implementations. However, this approach does not scale well. When dealing with more complex state machines, it can lead to code that is cumbersome, hard-to-understand and challenging to maintain.
The State pattern provides a robust way to implement a state machine. State-specific behaviour is encapsulated by a set of types that conform to the same interface. The entity being modeled relies on these types to handle events that may trigger transitions between its states.
State pattern
1. Intent
The State pattern enables an instance of a type to change its behaviour with changes in its internal state. From an external perspective, the instance may appear to change its type.
2. Structure and participants
The class diagram below illustrates the structure of the State pattern:
The Context type provides the interface to be used by clients. The State type defines an interface, to be used by the context, which represents state-specific behaviour of the context. Each concrete state type encapsulates the behaviour associated with a particular state of the context. The context is thus able to delegate state-specific behaviour to the concrete state types.
The flexibility of the pattern comes from the fact that the context depends only on the interface defined by the State type and not on any of the concrete state types. Instances of concrete state types can thus be swapped in and out as the context transitions from one state to another.
State machine diagram
A state machine diagram, or state diagram, is a visual representation of a state machine. Each state is represented by a rounded rectangle and arrows connecting the rectangles show transitions between states. If the state machine has a default initial state, a small filled circle is used to represent an initial pseudostate, which typically transitions automatically into the first proper state.
Let’s assume we have been tasked with building the control unit for a security system, which sounds an alarm when there is an intrusion or the threat of an intrusion. The security system will be connected to sensors, each of which can detect a breach, such as a door or window being opened. When the system is armed, such a breach should cause an alarm to be sounded. The system will also have one or more panic switches, which can be used to sound an alarm even when there is no actual breach but there may be an imminent threat of a breach from a potential intruder. When the system is not armed, a breach or panic signal should not have any effect.
We also need to ensure that, once armed, the system can be disarmed only through the use of a code. This code will be set when the system is initialized and must be used to disarm the system. The same code must also be used to reset the system, i.e., stop the alarm once it has been sounded.
If we think of the security system described above as a state machine, we can represent it visually by the following state diagram:
Although the diagram is fairly intuitive, let’s briefly look at its constituent parts and some of the formalisms governing construction of state diagrams, as per the Unified Modeling Language (UML) specification.
1. States, events and transitions
Our system can be in any of three states: Disarmed, Armed and Alarm. The Disarmed state is the default initial state.
Transitions between the states can be triggered by the following events:
- Arm: Triggers a transition from the Disarmed state to the Armed state.
- Disarm: Triggers a transition from the Armed state to the Disarmed state.
- Breach: Indicates that a sensor has been breached. Triggers a transition from the Armed state to the Alarm state.
- Panic: Indicates that a panic switch has been activated. Triggers a transition from the Armed state to the Alarm state.
- Reset: Triggers a transition from the Alarm state to the Disarmed state.
2. Entry and exit activities for states
Any of the states in a state machine may have entry and/or exit activities. An entry activity is performed just after a state is entered while an exit activity is performed just before a state is exited. Specifying activities for states is not mandatory but it is a useful way of ensuring that states encapsulate the behavior associated with them.
3. Transition effects and guard conditions
A complete specification of a transition can have four parts – a trigger event, a guard condition, an effect and a target state. The trigger event is what causes the transition to fire and the target state is the state that becomes active on completion of the transition. The guard condition is an optional boolean expression which is evaluated when the relevant trigger event occurs. If a transition has a guard condition, it will fire only if the boolean expression evaluates to true. An effect is an optional action or activity that is performed before the transition completes, i.e., before the target state becomes active. A transition label is used to represent the trigger event, guard condition and effect in the format: ‘event [guard condition] / effect’.
State Pattern implementation
1. The Context type
Having visualized our state machine, let’s start implementing it by defining the Context type, which has methods corresponding to events in the state diagram.
class System {
func arm() {}
func disarm(usingCode code: String) {}
func breach() {}
func panic() {}
func reset(usingCode code: String) {}
}
What we have at the moment is just a skeleton of the Context type, with empty implementations of the methods. This is because we are yet to put in place the machinery of the State pattern, which will be used by the Context type to provide the required functionality.
2. The State interface
When describing the structure of the State pattern, we had noted that the context delegates state-specific behavior to the concrete state types, using methods defined by the State interface, to which all the concrete state types conform.
Each concrete state type will represent a single state of the context. It will be responsible not only for handling events that may trigger transitions from that state but also for executing the entry and exit activities of the state. These are separate responsibilities but we need to combine them into a single interface. Swift allows us to do this cleanly using protocol composition, which lets us combine the requirements of two or more protocols.
protocol Events {}
protocol Activities {}
typealias State = Events & Activities
We have defined separate protocols to represent events, which may trigger transitions between states, and activities related to the states themselves. We combine the requirements of these protocols into a single protocol, which we bind to the name State using a typealias. This demonstrates how protocol composition can facilitate following the Interface Segregation Principle to define protocols narrowly while being able to combine their requirements to meet the needs of the application.
Having worked out the overall scheme, we define the requirements of the Events protocol.
protocol Events {
mutating func arm(_: System) -> Transition?
mutating func disarm(usingCode: String, _: System) -> Transition?
mutating func breach(_: System) -> Transition?
mutating func panic(_: System) -> Transition?
mutating func reset(usingCode: String, _: System) -> Transition?
}
This protocol contains one method for each of the events that may trigger a transition from one state to another. Each method takes an instance of the Context type as a parameter, to be able to call methods on the system to effect actions as required. Note that all the methods are marked with the keyword mutating. This is because we will use structs to model the concrete states and we want to account for the possibility that any of the methods may need to mutate the state of its concrete state instance.
Not every state in a state machine may have a possible transition to every other state. In our case, for instance, when the alarm system is in the Disarmed state, a breach or a panic event should not have any effect. Similarly, a breach or panic event should have no effect on a system already in the Alarm state. An otherwise valid transition may also fail due to a guard condition not evaluating to true. This would be the case if an attempt is made to trigger a transition from the Armed or Alarm States to the Disarmed state, without providing a valid code.
This is why the return type of all the above methods is Transition?. This allows each concrete type to return a suitable transition, with the option to return nil if a transition is not possible.
Here is the Transition type, which specifies the target state of the transition and an optional effect.
struct Transition {
var targetState: State
var effect: Effect? = nil
typealias Effect = (System) -> ()
}
We use a protocol extension to provide default implementations of all the methods of the Events protocol. All these implementations return nil to cover the case where a transition is not possible. This will let the concrete state types implement methods only for the events that can actually cause a transition from that state.
extension Events {
mutating func arm(_: System) -> Transition? { return nil }
mutating func disarm(usingCode: String, _: System) -> Transition? { return nil }
mutating func breach(_: System) -> Transition? { return nil }
mutating func panic(_: System) -> Transition? { return nil }
mutating func reset(usingCode: String, _: System) -> Transition? { return nil }
}
Next, we define the requirements of the Activities protocol.
protocol Activities {
func enter(_: System)
func exit(_: System)
}
These methods will allow each concrete state type to define entry and exit activities for the state it represents. Both these methods take an instance of the Context type as a parameter, to be able to call methods on the system to effect any required actions.
Since every state will not have entry and/or exit activities, we define the following extension to the above protocol.
extension Activities {
func enter(_: System) {}
func exit(_: System) {}
}
With default empty implementations of both the methods in place, concrete state types will need to implement these methods only for states that actually have entry and/or exit activities respectively.
3. The Context type revisited
Now we are in a position to implement the methods in the Context type. The Context type will delegate state-specific behaviour to the concrete state types using the interface defined by the State type.
class System {
init(code: String) {
self.code = code
state = DisarmedState()
}
func arm() {
process(transition: state.arm(self))
}
func disarm(usingCode code: String) {
process(transition: state.disarm(usingCode: code, self))
}
func breach() {
process(transition: state.breach(self))
}
func panic() {
process(transition: state.panic(self))
}
func reset(usingCode code: String) {
process(transition: state.reset(usingCode: code, self))
}
func isValid(code: String) -> Bool {
let isValid = code == self.code
print(isValid ? "Code accepted" : "Invalid code")
return isValid
}
private func process(transition: Transition?) {
guard let transition = transition else { return }
state.exit(self)
transition.effect?(self)
state = transition.targetState
state.enter(self)
}
private var state: State
private let code: String
}
The state property of the Context type is the lynchpin of the State pattern. As the program runs and the context transitions from one state to another, instances of different concrete state types will be assigned to this property. State-related behaviour will thus be delegated using this property to the relevant concrete state instances.
The initial state of the state machine is set in the initializer of the Context type. The context does not have to know when to execute a transition and to which state. That knowledge will be encapsulated in the concrete state types, as we will see in the next section. All the context has to do is to use the return value of the functions called on the concrete state types to transition to the next state. This is done in the process(transition:) method, which does nothing if the argument is nil and executes a transition when one is available.
4. Concrete state types
Having laid the groundwork, we are ready to implement state-related behaviour in the concrete state types, each of which will conform to the State protocol.
Let’s start with the simplest concrete state type, which represents the Disarmed state.
struct DisarmedState: State {
func enter(_: System) {
print("System disarmed")
}
func arm(_ system: System) -> Transition? {
return Transition(targetState: ArmedState())
}
}
The enter(_:) method, which will be executed when this state is entered, disarms the system. For the purposes of this demonstration, it simply prints a message to the console stating that the system has been disarmed.
When the system is disarmed, a breach or a panic event does not apply. Neither can the system be reset from this state. The only applicable method from the Events protocol, therefore, is arm(_:), which causes the system to become armed by returning a transition with an instance of ArmedState as the target state.
This is the state we will implement next.
struct ArmedState: State {
func enter(_: System) {
print("System armed")
}
func disarm(usingCode code: String, _ system: System) -> Transition? {
guard system.isValid(code: code) else { return nil }
return Transition(targetState: DisarmedState())
}
func breach(_ system: System) -> Transition? {
return Transition(targetState: AlarmState())
}
func panic(_ system: System) -> Transition? {
return Transition(targetState: AlarmState())
}
}
The enter(_:) method in this case arms the system. The disarm(usingCode:_:) method disarms the system by returning a transition with an instance of DisarmedState as the target state, provided the correct code has been entered. If the code is not correct, nil is returned to indicate that a transition should not take place. The breach(_:) and panic(_:) methods return transitions with instances of AlarmState as the target state.
The last state we need to implement is the Alarm state.
struct AlarmState: State {
func enter(_: System) {
print("Alarm sounded")
}
func exit(_: System) {
print("Alarm stopped")
}
func reset(usingCode code: String, _ system: System) -> Transition? {
guard system.isValid(code: code) else { return nil }
return Transition(targetState: DisarmedState())
}
}
The enter(_:) method for this state sounds the alarm. This is the only state with an exit(_:) method, which stops the alarm. Once the alarm has been sounded, any further breach or panic event should not cause a transition, so these events don’t apply to this state. The reset(usingCode:_:) method returns a transition with an instance of DisarmedState if the code provided is valid, returning nil otherwise.
5. Test run
First, we test the system in its initial Disarmed state. In this state, a breach reported by a sensor or the press of a panic button should not have any effect. So we call the two methods on a newly minted System instance and see what happens.
let system = System(code: "1234")
system.breach()
system.panic()
As expected, nothing gets printed to the console.
Let us arm the system and then trigger a breach.
system.arm() // System armed
system.breach() // Alarm sounded
system.reset(usingCode: "1234") // Code accepted
// Alarm stopped
// System disarmed
This time, when the breach() method is called, the alarm is sounded as expected. We then call the reset(usingCode:) method to stop the alarm and disarm the system.
Let’t see if the panic switches also work as expected.
system.arm() // System armed
system.panic() // Alarm sounded
system.reset(usingCode: "1234") // Code accepted
// Alarm stopped
// System disarmed
A call to the panic() method has the same effect as the breach() method earlier. As before, we call the reset(usingCode:) method to stop the alarm and disarm the system.
Now let’s arm the system and then try to disarm it, first with an invalid code and then with the correct code.
system.arm() // System armed
system.disarm(usingCode: "0000") // Invalid code
system.disarm(usingCode: "1234") // Code accepted
// System disarmed
When we use the wrong code, the state of the system remains unchanged and we just get a polite error message. When we use the correct code, the system gets disarmed.
Finally, let’s make sure that once we are in the Alarm state, the system can be reset only using the correct code.
system.arm() // System armed
system.breach() // Alarm sounded
system.reset(usingCode: "1111") // Invalid code
system.reset(usingCode: "1234") // Code accepted
// Alarm stopped
// System disarmed
As expected, when we enter an invalid code, it is rejected. The correct code is accepted, stopping the alarm and disarming the system.
We are done. It is time to unveil the security system to the world and let the orders roll in.
Dealing with Change
It is said that change is the only constant in life. This is probably more true in systems development than in many other situations. Most real-world systems have to deal with change and the true test of the robustness of a design is how well it can embrace change.
We have been selling our security system for some time and the inevitable change requests are beginning to come in. Let’s see how the State pattern allows our system to flex as business requirements change.
1. Adding a transition
In the initial design of the security system, if a sensor is breached or a panic button is pressed while the system is disarmed, there is no effect. Some customers have asked for a design change where a panic can be triggered even when the system is disarmed. This panic functionality could be used, even when the system is disarmed, to scare off any unsavoury characters who may be lurking around a property secured by one of our security systems.
We modify the state diagram we had drawn earlier, to reflect the new requirement:
The only change from the earlier state diagram is a new transition from the Disarmed state to the Alarm state, triggered by a panic event.
To effect this change, we need to extend the behaviour of the system only when it is disarmed. With the behaviour related to every state encapsulated in its own type, all we have to do is add the following method to the DisarmedState struct.
func panic(_ system: System) -> Transition? {
return Transition(targetState: AlarmState())
}
Voila! Our system will now go into the Alarm state when a panic is triggered in the Disarmed state.
let system = System(code: "1234")
system.panic() // Alarm sounded
So far so good.
Time to crank our change request engine up a few notches.
2. Adding properties to concrete state types
As the fame of our security system has spread, more and more businesses are beginning to use it. Being more security conscious than home owners, their requirement is that when the system is in the Armed state, the user should be given only a limited number of attempts to try and disarm it. If the user makes more than the permitted number of attempts using an invalid code, the system should sound the alarm.
We dust off our last state diagram and make the required changes:
We have added a new transition from the Armed state to the Alarm state, which is triggered by the disarm event, but has a guard clause and fires only if the code provided is not valid and the maximum number of permitted attempts has been exceeded.
The only changes we will need to make will be to the ArmedState struct.
First, we add the following stored properties to enable tracking of the number of attempts made to disarm the system and to ensure that the maximum is not exceeded.
private var disarmAttempts = 0
private let maxDisarmAttempts = 3
Then, we modify the disarm(usingCode:_:) method to increment disarmAttempts for each call and to return a transition with an instance of AlarmState if the maximum number of attempts is exceeded.
mutating func disarm(usingCode code: String, _ system: System) -> Transition? {
disarmAttempts += 1
if system.isValid(code: code) {
return Transition(targetState: DisarmedState())
} else if disarmAttempts > maxDisarmAttempts {
return Transition(targetState: AlarmState())
} else {
return nil
}
}
Note that this is the first time we have used a mutating method in a concrete state type. However, since we have already accounted for this possibility in all the methods in the Events protocol, there is no need to make any changes to the protocol.
That’s all we need to do. Time for some testing.
let system = System(code: "1234")
system.arm() // System armed
system.disarm(usingCode: "0000") // Invalid code
system.disarm(usingCode: "1111") // Invalid code
system.disarm(usingCode: "2222") // Invalid code
system.disarm(usingCode: "3333") // Invalid code
// Alarm sounded
Sure enough, we are allowed three attempts with an invalid code, but the moment we try a fourth time, the alarm sounds.
3. Adding a transition effect
Business users are a demanding bunch. Now they want that every time a user exceeds the maximum number of attempts permitted to disarm the system, in addition to sounding the alarm, the system should inform the system administrator.
This presents an interesting situation. There are two transitions that leave the Armed state, one leading to the Disarmed state and the other to the Alarm state. The exit activity of the Armed sate, which disarms the system, applies regardless of which transition fires. The requirement to inform the administrator, however, would apply only to the transition that leads to the Alarm state, which would be triggered if the maximum number of attempts allowed to disarm the system is exceeded.
When we have to add an action or activity to be executed only for a certain transition leaving a state but not for other transitions leaving the same state, we need to use a transition effect. This is executed after the exit activity of the state being exited has been executed but before executing the entry activity of the target state.
Given below is the modified state diagram with a transition effect added in the correct place.
Implementing this in code is quite simple. Our Transition type allows us to add an optional transition effect. Until now, we have not had to use one, so we have simply been ignoring the effect parameter of the Transition initializer, since the property has a default value of nil. To add a transition effect, all we need to do is to provide a closure with the activity to be performed, using the trailing closure syntax.
We modify the disarm(usingCode:_:) method of the ArmedState struct, to add the required effect to the transition triggered when the code provided is not valid and the value of disarmAttempts exceeds the value of maxDisarmAttempts.
mutating func disarm(usingCode code: String, _ system: System) -> Transition? {
disarmAttempts += 1
if system.isValid(code: code) {
return Transition(targetState: DisarmedState())
} else if disarmAttempts > maxDisarmAttempts {
return Transition(targetState: AlarmState()) { system in
print("Administrator informed")
}
} else {
return nil
}
}
This is it. Let’s see if it works.
let system = System(code: "1234")
system.arm() // System armed
system.disarm(usingCode: "0000") // Invalid code
system.disarm(usingCode: "1111") // Invalid code
system.disarm(usingCode: "2222") // Invalid code
system.disarm(usingCode: "3333") // Invalid code
// Administrator informed
// Alarm sounded
This time, when we make the fourth attempt to disarm the system using an invalid code, the administrator is informed and then the alarm is sounded.
4. Adding a new state
With the fame of our security system spreading, it has attracted the attention of establishments such as banks, with more complex security requirements.
Home and normal business owners use the panic feature, which can be used to sound the alarm when the system is disarmed, to scare off random unsavoury characters. Banks, on the other hand, face the possibility of armed robbers entering the premises during normal hours of operation, when the system would be disarmed. To deal with such situations, banks typically install panic buttons out of public view, which can easily be activated by staff. However, if armed robbers have already entered the premises, it may not be wise to sound the alarm. What would be more useful is a mechanism which would trigger a message to law enforcement personnel, without sounding the alarm.
We know the drill. Let’s modify our state diagram to visualize the new requirement:
Our state diagram now sports a new Silent Alarm state, which results from a panic event when the system is in the Disarmed state. When the system is in the Silent Alarm state, it can be reset in the same way as for the Alarm state.
We define a new concrete state type called SilentAlarmState which, like all other concrete state types, conforms to the State protocol.
struct SilentAlarmState: State {
func enter(_: System) {
print("Police informed")
}
func reset(usingCode code: String, _ system: System) -> Transition? {
guard system.isValid(code: code) else { return nil }
return Transition(targetState: DisarmedState())
}
}
The enter(_:) method informs the police. No exit(_:) method is required. The reset(usingCode:_:) method is the same as in the Alarm state, which ensures that the system must be reset using the correct code.
The only other thing we have to do is to modify the panic(_:) method in the DisarmedState struct, as shown below.
func panic(_ system: System) -> Transition? {
return Transition(targetState: SilentAlarmState())
}
That’s it. Time for a test-drive.
let system = System(code: "1234")
system.panic() // Police informed
system.reset(usingCode: "1234") // Code accepted
// System disarmed
Everything works as expected and we can now sell our system to banks and other high-security establishments.
The above examples show the flexibility that the State pattern gives us to accommodate changing requirements with only localized changes in code, without having the effects of these changes ripple through the code base.
Key design and implementation considerations
1. Handling state transitions
A signature characteristic of the State pattern is transition between states, which leads to the question: Which participant should be responsible for deciding when to transition from one state to another, and for knowing which state to transition to under what conditions? This responsibility could be given to either the Context type or the concrete state types themselves. While it is possible to make the context responsible for controlling state transitions, that would concentrate too much responsibility in the context. This could lead to creating just the kind of complex and hard-to-modify logic in the context that the State pattern aims to avoid.
The more common approach, which we have followed, is to let the concrete state types encapsulate the state transition logic. This does create a bit of dependency among the concrete state types, since each concrete state type must be aware of at least one other concrete state type. However, the benefit in making the transition logic decentralized and easy to modify makes this minor compromise worthwhile.
It is pertinent to note that while the context in our approach does not decide when a transition is to happen and to which state, we have given responsibility to the context to process the transitions. This ensures that each transition takes place in a well-orchestrated and orderly manner, with the specific steps required to complete transitions specified in one place and not sprinkled throughout the concrete state types.
2. States with state
Another question that arises when implementing the State pattern is whether the concrete state types should have stored properties, or put another way, whether the states should have state of their own. It is perfectly acceptable to define concrete state types that have stored instance properties. These properties can be used by the concrete state types in discharging their responsibilities.
It is especially interesting when some of these properties are mutable, allowing refinement of behaviour, depending on the values of these properties. In our example, as part of dealing with one of the change requests, we gave the ArmedState struct one mutable and one immutable property. These allowed us to add a conditional transition from the Armed state to the Alarm state.
3. Creating states
Each transition needs an instance of the target concrete state type, which means we need to decide where to put state creation logic. There is an argument for putting this logic in the Context type. However, this would mean that the context would have knowledge of individual concrete state types and will have to change when we need to add or remove a state. The approach we have taken is to let the concrete state types directly create instances of other concrete state types, eliminating the need for the context to know anything about concrete states. The context processes transitions between states using only the State interface, without knowing which concrete state types are involved.
4. Events and activities not applicable to certain states
Implementing a state machine means having to deal with a lot of optionality. It is quite common to have trigger events that don’t apply to certain states. Every state can specify entry and exit activities, but may not. Similarly, every transition can specify an effect, but may not. Having to implement methods for each of these possibilities could lead to a proliferation of methods that don’t do anything.
We have tackled this issue in two ways. First, by using protocol extensions, we have provided default implementations of all the methods related to trigger events as well as entry and exit activities of states. So concrete state types need to implement methods related only to those trigger events and activities that actually apply to them. Second, we have used an optional closure to specify the transition effect, with a default value of nil. This means that an effect needs to be specified only by transitions that actually have an effect and not otherwise.
5. Specifying activities in states or transitions
When a transition fires, it can cause a number of things to happen, including an exit activity for the state from where the transition got triggered, an entry activity for the target state, and an effect that is external to the states. The latter case is encountered when there are activities or actions that must be performed after the state from which the transition was triggered has been exited but before the target state of the transition is entered.
This gives fine-grained control over how to specify and sequence various activities resulting from transitions. It also affords an opportunity to think about whether an action or activity should be specified as an activity in a state or as a transition effect external to any state. Specifying actions and activities inside states can help stop data and logic that should be encapsulated in states from leaking out into transitions. It can also ensure that each state is able to perform any setup activities, which processes inside the state may rely on, when it is entered and it can perform any cleanup activities before they it is exited. Any actions or activities that belong outside the states can be specified as transition effects, to be performed, where required, between exit and entry activities of the relevant states.
Benefits of the State pattern
1. Clear control flow
Compared to the alternative of using simple data values to represent states and making the context responsible for deciding what action to perform for each event based on these data values, the State pattern delivers much clearer control flow. In fact, the context is no longer responsible for the control flow. All it has to do is delegate state-specific behaviour to its state property, letting the concrete instance it holds swing into action to provide the required functionality.
2. Partitioning behaviour
The State pattern lets us distribute the behavior of what could otherwise become a monolithic unit of code to a number of concrete state types, each responsible for encapsulating the data and logic related to a single state of the context. This leads to a neat partitioning of behaviour, with all behaviour related to a particular state localized in a single type. This includes not only the entry, exit and other activities associated with the state but also logic related to when it is possible to transition from the state and determining the target state for each transition.
3. Flexibility and extensibility
As we have already seen, the State pattern makes it easy to modify and extend behaviour. Whether modifying the behavior of an existing state or adding a completely new state, the State pattern helps localize the effect of the change. This is thanks to decoupling of the context from the concrete state types that contain the logic related to the various states of the context. The context only uses the methods defined by the State interface, and is not dependent on any of the concrete state types. In fact, with the concrete state types taking responsibility for creating their own instances, the context does not even need to be aware of how many concrete state types actually exist.
Practical applications of the State pattern
Here is a sampling of some applications of the State pattern, discovered and described by practitioners and subject matter experts:
1. Database updates
[Larman] discusses employing the State pattern to simplify the logic used to commit or rollback changes made to records in a database. At any given time, a database record in memory may be new (not in the database) or old (retrieved from the database). An old record could be clean (unmodified) or dirty (modified).
Based on the state of a record, it should react differently to a commit or a rollback operation. For example, on a commit operation, an “old dirty” record should be updated to the database, because it has been modified since being retrieved, while an “old clean” record should simply be ignored since it has not been modified.
2. Drawing programs and frameworks
[Gamma] talks about how the State pattern can be used to create interactive drawing programs and frameworks. In a typical drawing program, the user has a palette of “tools” to select from, which may include a drawing tool, a paint tool, a selection tool, a text tool, etc. Based on which tool the user has selected, user actions such as a mouse click, mouse press and release, keystroke input, etc., may be handled differently.
When the drawing tool is active, for instance, a mouse press may start the creation of a new shape while a mouse release may give the shape its final size and position. When the selection tool is active, the same mouse press may be used to mark the start of a rectangular area within which all shapes will be selected, while a mouse release may complete the selection.
3. Graphical user interfaces
[Martin] notes how the State pattern can be used to capture and implement high-level application policies for GUIs, making them explicit and easy to maintain. Modern GUIs can present a plethora of information in a single screen. However, not all information that could be presented may be applicable, relevant or even permitted for display at any given time. Similarly, certain tools or menu options could be made unavailable at certain times by disabling them or not displaying them altogether. For instance, different information and functionality will usually be available depending on whether the user is just browsing or logged in with a password. For multi-stage actions, the screen presented to the user may be different depending on what stage the user has reached.
4. Network protocols
[Gamma] provides a worked example of how the State pattern can be used to model a TCP connection. An object representing a TCP connection can be in one of several different states, e.g., established, listening, closed. Depending on the current state of the connection, it will react differently to requests, such as a request to open or close a connection.
5. Game programming
[Nystrom] describes how the State pattern can be used to control the behaviour and graphical representation of game characters, based on their current state. A character in an adventure game, for instance, can be in various states, such as standing, running, ducking, diving, sliding, etc. Depending on the state, the character would be represented differently on the screen and would react differently to various commands.
State pattern implementations could even be used concurrently to model not only the state of a character but also what equipment she may be carrying. The same fire command could then be used to release an arrow or fire a gun, depending on what weapon the character is wielding at the time.
6. Embedded systems
A number of real-world machines have distinct states, which determine whether and how the machine responds to a particular command or event.
[Freeman] gives an example of a gumball machine, which may or may not have gumballs to dispense and may or may not have coins inserted, so will react differently when the crank is turned depending on its state. Similar logic would apply to most vending machines.
[Martin] looks at a turnstile, the likes of which are found in places ranging from train stations and museums to lobbies of office buildings. A turnstile may be in a locked or unlocked state, may change state based on a valid ticket being inserted or an entry pass being scanned, and may or may not let a person through depending on its state.
Conclusion
A state machine is a useful abstraction to model entities commonly found in a number of real-world domains. The State pattern provides a robust way to implement a state machine. Owing to encapsulation of the behaviour related to individual states and decoupling of the context from the concrete state types, there is clearer control flow and partitioning of behaviour, leading to more flexible and maintainable applications.
References
[Gamma] | Gamma, E., Helm, R., Johnson, R. & Vlissides, J. (1995). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley |
[Freeman] | Freeman, E., Freeman, E., Sierra, K. & Bates, B. (2004). Head First Design Patterns. O’Reilly Media |
[Larman] | Larman, C. (2005). Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development (3rd ed.). Prentice Hall |
[Martin] | Martin, R. C. (2003). Agile Software Development: Principles, Patterns and Practices. Prentice Hall |
[Nystrom] | Nystrom, R. (2014). Game Programming Patterns |
Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post or start a conversation on Twitter
.
Lang says
Great article, very in-depth and well explained, would love to read more article like this.
BTW, may I ask what tool are you using to draw the diagram?
Khawer Khaliq says
Hi Lang,
Many thanks. I am glad that you liked the article. My articles generally tend to look at a topic in a fair bit of depth. This is not to everyone’s liking but is appreciated by people looking to work through a topic. More articles will be coming in due course, including on the application of design patterns in Swift.
For drawing, I use SmartDraw (https://www.smartdraw.com). It is easy to use, fairly powerful and can be used across devices, even operating systems, since it is browser based.
Bruce says
The `Events` protocol is designed like this may be better.
“`protocol Events {
func execute(by system: System) -> State
}
“`
and those events implement this protocol, for example:
“`
struct PanicEvent: Events {
func execute(by system: System) -> State {
return AlarmState()
}
}
“`
What do you think?
Eric says
If you’re interested, as I worked through your well-written article, I created a couple of swift playgrounds implementing the state machine.
https://github.com/ericg-learn-apple/state_machine