First, let's see how this reader could look if we implement its state with an enum:
class CardReader {
Implement the state with the enum, with all the possible state values, as follows:
enum State {
case unknown
case waiting
case reading
case read(CardInfo)
case failed(Error)
}
Use theĀ private var state property so that no one can mutate the state from outside:
private var state: State = .unknown
The main routine is implemented as follows. For each loop, it will print the current state:
func start() {
while true {
print("\(state)")
switch state {
case .unknown:
state = .waiting
case .waiting:
When we're waiting, we wait for a card to be detected by the radio. When the card is detected, we change the state to reading:
if seenCard() {
state = .reading
}
case .reading:
When reading, we'll wait for this to complete. If it succeeds, we will change the state to read, or otherwise indicate the failure as follows:
if readCard() {
state = .read(CardInfo())
} else {
state = .failed(ReadError())
}
case .read(_):
Now that the card is read, we can apply the logic. For example, that may involve using an external object to open doors, display a success message on a screen, decrement the number of journeys left on the card, or something more complex.
In any case, whether this logic is implemented in the enum or the context, you can clearly see that the state transitions are polluted by each state's logic. The same is true for the failure scenario, as follows:
// Card is read
// Now we can open the gate
state = .waiting
case .failed(_):
// Display an error message on the screen
// Prompt to restart after a few seconds
state = .waiting
}
sleep(1)
}
}
}
We'll start the reader as follows:
let reader = CardReader()
reader.start()
This program should print, depending on the output of readCard() and seenCard(), something along the following lines:
unknown
waiting
reading
failed(ReadError())
waiting
reading
failed(ReadError())
waiting
reading
read(CardInfo(id: 415))
waiting
waiting
reading
failed(ReadError())
...
As we've seen, all the logic is packed into the context, making it very inefficient and hard to maintain. The state design pattern provides a solution for this problem. Let's refactor the code to improve maintainability.