Behavioural Design Pattern
Kotlin Design Patterns: State Explained
Purpose of the pattern
It’s used to alter the behavior of an object based on its State. Think of it as a finite-state machine. Depending on the current State, the object behaves differently.
What do we get from that?
- Single Responsibility, each separated class is responsible for a given State.
- Open/Closed, each state-specific behaviour is defined independently. It means introducing new states and behaviours without altering existing state classes.
- Finite-State Machine, which is understandable by programmers and easy to document.
Implementation
Context
stores State
and delegates work to ConcreteState
that is hidden behind State
. State
defines a set of operations that ConcreteState
implements differently depending on a given responsibility.
Moreover, ConcreteState
can depend on Context
to be able to change the State. However, it’s better to avoid it if possible as the classes become tightly coupled.
It works like a Finite-State Machine:
If your code behaves like a finite-state machine, refactoring it to fit into this pattern is worth it.
Example
Your task is to create a program simulating Traffic Lights. It’s a perfect case for the State because Traffic Lights change like a finite-state machine! For simplicity, this is how our lights will work:
We’ll structure our code like this:
In our example, we want the State
implementations to depend on TrafficLight
so that they can change their internal state to the next light.
Let’s start with the State
interface because everything depends on it:
interface State {
fun carAction()
}
Now, we need a TrafficLight
because State
implementations depend on it:
class TrafficLight {
// Green by default
private var state: State = GreenLightState(this)
fun changeState(state: State) {
this.state = state
}
fun carAction() {
state.carAction()
}
}
And finally the State
implementations, they’re very similar, printing different messages and updating to a different light state:
class GreenLightState(private val trafficLight: TrafficLight) : State {
override fun carAction() {
println("GREEN: Cars are driving")
trafficLight.changeState(YellowLightState(trafficLight))
}
}
class YellowLightState(private val trafficLight: TrafficLight) : State {
override fun carAction() {
println("YELLOW: Cars are starting to brake")
trafficLight.changeState(RedLightState(trafficLight))
}
}
class RedLightState(private val trafficLight: TrafficLight) : State {
override fun carAction() {
println("RED: Cars are waiting")
trafficLight.changeState(GreenLightState(trafficLight))
}
}
Here’s how to use it:
fun main() {
val light = TrafficLight() // It start as green
light.carAction() // GREEN: Cars are driving
light.carAction() // YELLOW: Cars are starting to brake
light.carAction() // RED: Cars are waiting
light.carAction() // GREEN: Cars are driving
}
As you can see, it updates as a finite-state machine!
In this pattern, consider making State
interface sealed and keeping all implementations inside a Single file if that’s okay for your project:
sealed interface State {
fun carAction()
class GreenLightState: State { ... }
class YellowLightState: State { ... }
class RedLightState: State { ... }
}
// Now you can access the State implementations by
// State.name for example State.GreenLightState
Thanks for reading! Please clap if you learned something, and follow me for more!
Learn more about design patterns:
Based on the book:
“Wzorce projektowe : elementy oprogramowania obiektowego wielokrotnego użytku” — Erich Gamma Autor; Janusz Jabłonowski (Translator); Grady Booch (Introduction author); Richard Helm (Author); Ralph Johnson (Author); John M Vlissides (Author)