In this tutorial, we will cover shortly how to implement the strategy and state pattern in Kotlin. As both patterns are well known in the software industry we will not discuss their pros and cons, as this is done already hundreds of times. The example will be used directly from the textbook Design Patterns: Elements of Reusable Object-Oriented Software. We encourage to read this book as it brings a lot of valuable content to the software industry.
Both, the strategy pattern and state pattern, are closely related. Therefore we will cover them together in the same Kotlin tutorial.
Strategy Pattern
One of the most common patterns used in nowadays software is the so-called strategy pattern. It is also known under the name “policy pattern”. Thanks to its simplicity it is easily understood by most developers. Furthermore, it helps to implement scalable software solutions which are compliant with the SOLID principles of object-oriented software design.
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
GoF, Design Patterns
Example Description
This pattern is used when clients will use different algorithms for the same problem. One of the problems which often occurs is to break a stream of text (for instance a big string) into smaller strings. We call this a line break algorithm. Depending on the context one might use different algorithms. The clients, who use the code should not be dependent on the concrete implementation.
We will encapsulate each of the algorithms into a separate class (called a strategy). Each of these classes must implement a certain interface, which will be used (directly or indirectly) by the client.
In our case, we will handle three different algorithms SimpleCompositor
, TeXCompositor
and ArrayCompositor
. The client class (Composition
) will use the abstract class Compositor
. The following UML diagram illustrates the class setup.
Implementation in Kotlin
To implement the above example in Kotlin we would use an abstract interface Compositor
. All classes which will be used as algorithms must implement this interface.
interface Compositor { fun compose() } class SimppleCompositor : Compositor { override fun compose() { println("SimppleCompositor::compose") } } class TeXCompositor : Compositor { override fun compose() { println("TeXCompositor::compose") } } class ArrayCompositor : Compositor { override fun compose() { println("ArrayCompositor::compose") } } class Composition(private var compositor: Compositor) { fun traverse() { compositor.compose() } fun repair() { compositor.compose() } } fun main() { val teXCompositor = TeXCompositor() val composition = Composition(teXCompositor) composition.repair() }
State Pattern
This pattern is closely related to the strategy pattern. We want to be able to change the behavior of an object when its state changes. This change in behavior should be transparent to the clients which will be using the class.
Allow an object to alter its behavior when its internal state changes.The object will appear to change its class.
Gof, Design Patterns
Example Description
Consider a class TCPConnection
that represents a TCP network connection. It can have several states such as Established, Listen, and Closed. Depending on the state it will work (behave) differently. We will encapsulate each state (and its functionality) in one class. All classes (states) must implement the interface provided by the abstract class TCPState
. The following UML diagram illustrates the class setup.
Note that in contrast to the original design we have TCPConnection
as well implement the interface provided by TCPState
.
Implementation in Kotlin
The pattern is similarly implemented as the previous example. We will use an abstract interface that provides the function declarations. All states will implement this interface. The TCPConnection
class has a state member and delegates all function calls directly to its state.
Note that we can not use Kotlin’s built-in Delegation pattern. As described in the tutorial it is currently not possible to change the delegate once it was assigned to an object. In other words, we are not able to change the state using this pattern. This means that we have to implement the interface manually.
interface TCPState { fun open() fun close() fun acknowledge() } class TCPEstablished : TCPState { override fun open() { println("TCPEstablished::open") } override fun close() { println("TCPEstablished::close") } override fun acknowledge() { println("TCPEstablished::acknowledge") } } class TCPListen : TCPState { override fun open() { println("TCPListen::open") } override fun close() { println("TCPListen::close") } override fun acknowledge() { println("TCPListen::acknowledge") } } class TCPClosed : TCPState { override fun open() { println("TCPClosed::open") } override fun close() { println("TCPClosed::close") } override fun acknowledge() { println("TCPClosed::acknowledge") } } class TCPConnection : TCPState { private var state : TCPState = TCPListen() override fun open() { state.open() } override fun close() { state.close() } override fun acknowledge() { state.acknowledge() } fun changeState() { // other state } } fun main() { val connection = TCPConnection() connection.open() connection.changeState() }