Behavioral Design Patterns in Kotlin: Simplified for Success
Design patterns are vital in developing software that is not only robust but also easy to maintain and scale. These patterns can be divided into three main categories: creational, structural, and behavioral. Behavioral patterns specifically focus on how objects interact with one another, manage their internal processes, and coordinate communication. By leveraging these patterns, developers can simplify complex behavior and make systems more adaptable to change.
In this blog, we’ll dive into several behavioral design patterns in Kotlin. We’ll explore each pattern with clear, easy-to-understand examples and explanations, helping you grasp the concepts without getting lost in technical jargon. Let’s get started and see how these patterns can improve your code!
What Are Behavioral Design Patterns?
Behavioral design patterns focus on how objects collaborate and share responsibilities. Unlike structural patterns, which deal with the composition of objects, behavioral patterns emphasize how objects interact and communicate with one another. These patterns help achieve loose coupling, allowing objects to work together without needing to know too much about each other’s inner workings.
Some of the most commonly used behavioral patterns include:
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
In this post, we’ll take a closer look at each of these patterns and demonstrate how they can be implemented using Kotlin.
Chain of Responsibility (CoR)
Design patterns are a cornerstone of writing clean, maintainable, and reusable code. One of the more elegant patterns, the Chain of Responsibility (CoR), allows us to build a flexible system where multiple handlers can process a request in a loosely coupled manner.
What is the Chain of Responsibility (CoR) Pattern?
The Chain of Responsibility design pattern is a behavioral design pattern that allows passing a request along a chain of handlers, where each handler has a chance to process the request or pass it along to the next handler in the chain. The main goal is to decouple the sender of a request from its receivers, giving multiple objects a chance to handle the request.
That means the CoR pattern allows multiple objects to handle a request without the sender needing to know which object handled it. The request is passed along a chain of objects (handlers), where each handler has the opportunity to process it or pass it to the next one.
Think of a company where a request, such as budget approval, must go through several levels of management. At each level, the manager can either address the request or escalate it to the next level.
Now imagine another situation: an employee submits a leave application. Depending on the duration of leave, it might need approval from different authorities, such as a team leader, department head, or higher management.
These scenarios capture the essence of the Chain of Responsibility design pattern, where a request is passed along a series of handlers, each with the choice to process it or forward it.
Why Use the Chain of Responsibility Pattern?
The Chain of Responsibility pattern offers several advantages:
- Decouples the sender and receiver: The sender doesn’t need to know which object in the chain will handle the request.
- Simplifies the code: It eliminates complex conditionals and decision trees by delegating responsibility to handlers in the chain.
- Adds flexibility: New handlers can be seamlessly added to the chain without impacting the existing implementation.
Structure of the Chain of Responsibility Pattern
Handler (Abstract Class or Interface)
Defines the interface for handling requests and the reference to the next handler in the chain.
abstract class Handler {
protected var nextHandler: Handler? = null
abstract fun handleRequest(request: String)
fun setNextHandler(handler: Handler) {
nextHandler = handler
}
}
- This defines an interface for handling requests, usually with a method like
handleRequest()
. It may also have a reference to the next handler in the chain. - The handler may choose to process the request or pass it on to the next handler.
ConcreteHandler
Implement the handleRequest()
method to either process the request or pass it to the next handler.
class ConcreteHandlerA : Handler() {
override fun handleRequest(request: String) {
if (request == "A") {
println("Handler A processed request: $request")
} else {
nextHandler?.handleRequest(request)
}
}
}
class ConcreteHandlerB : Handler() {
override fun handleRequest(request: String) {
if (request == "B") {
println("Handler B processed request: $request")
} else {
nextHandler?.handleRequest(request)
}
}
}
- These are the actual handler classes that implement the
handleRequest()
method. Each concrete handler will either process the request or pass it to the next handler in the chain. - If a handler is capable of processing the request, it does so; otherwise, it forwards the request to the next handler in the chain.
Client
Interacts only with the first handler in the chain, unaware of the specific handler processing the request.
fun main() {
val handlerA = ConcreteHandlerA()
val handlerB = ConcreteHandlerB()
handlerA.setNextHandler(handlerB)
// Client sends the request to the first handler
handlerA.handleRequest("A") // Handler A processes the request
handlerA.handleRequest("B") // Handler B processes the request
}
- The client sends the request to the first handler in the chain. The client does not need to know which handler will eventually process the request.
State Design Pattern
The State Design Pattern is part of the behavioral design patterns group, focusing on managing an object’s dynamic behavior based on its current state. As described in the Gang of Four’s book, this pattern “enables an object to modify its behavior as its internal state changes, giving the impression that its class has changed.” In short, it allows an object to alter its behavior depending on its internal state.
Key Features of the State Pattern
- State Encapsulation: Each state is encapsulated in its own class.
- Behavioral Changes: Behavior changes dynamically as the object’s state changes.
- No Conditionals: It eliminates long
if-else
orwhen
chains by using polymorphism.
Structure of the State Design Pattern
State pattern encapsulates state-specific behavior into separate classes and delegates state transitions to these objects. Here’s a breakdown of its structure:
State Interface
The State Interface defines the methods that each state will implement. It provides a common contract for all concrete states.
interface State {
fun handle(context: Context)
}
Here,
- The
State
interface declares a single method,handle(context: Context)
, which theContext
calls to delegate behavior. - Each concrete state will define its behavior within this method.
Concrete States
The Concrete States implement the State
interface. Each represents a specific state and its associated behavior.
class ConcreteStateA : State {
override fun handle(context: Context) {
println("State A: Handling request and transitioning to State B")
context.setState(ConcreteStateB()) // Transition to State B
}
}
class ConcreteStateB : State {
override fun handle(context: Context) {
println("State B: Handling request and transitioning to State A")
context.setState(ConcreteStateA()) // Transition to State A
}
}
ConcreteStateA
andConcreteStateB
implement theState
interface and define their unique behavior.- Each state determines the next state and triggers a transition using the
context.setState()
method.
Context
The Context is the class that maintains a reference to the current state and delegates behavior to it.
class Context {
private var currentState: State? = null
fun setState(state: State) {
currentState = state
println("Context: State changed to ${state::class.simpleName}")
}
fun request() {
currentState?.handle(this) ?: println("Context: No state is set")
}
}
- The
Context
class holds a reference to the current state viacurrentState
. - The
setState()
method updates the current state and logs the transition. - The
request()
method delegates the action to the current state’shandle()
method.
Test the Implementation
Finally, we can create a main function to test the transitions between states.
fun main() {
val context = Context()
// Set initial state
context.setState(ConcreteStateA())
// Trigger behavior and transition between states
context.request() // State A handles and transitions to State B
context.request() // State B handles and transitions to State A
context.request() // State A handles and transitions to State B
}
Output
Context: State changed to ConcreteStateA
State A: Handling request and transitioning to State B
Context: State changed to ConcreteStateB
State B: Handling request and transitioning to State A
Context: State changed to ConcreteStateA
State A: Handling request and transitioning to State B
Context: State changed to ConcreteStateB
How These Components Work Together
- The Context is the central point of interaction for the client code. It contains a reference to the current state.
- The State Interface ensures that all states adhere to a consistent set of behaviors.
- The Concrete States implement specific behavior for the Context and may trigger transitions to other states.
- When a client invokes a method on the Context, the Context delegates the behavior to the current state, which executes the appropriate logic.
Command Design Pattern
At its core, the Command Design Pattern decouples the sender (the one making a request) from the receiver (the one handling the request). Instead of calling methods directly, the sender issues a command that encapsulates the details of the request. This way, the sender only knows about the command interface and not the specific implementation.
In short,
- Sender: Issues commands.
- Command: Encapsulates the request.
- Receiver: Executes the request.
Structure of the Command Pattern
Before we dive into code, let’s see the primary components of this pattern:
- Command: An interface or abstract class defining a single method,
execute()
. - ConcreteCommand: Implements the Command interface and encapsulates the actions to be performed.
- Receiver: The object that performs the actual work.
- Invoker: The object that triggers the command’s execution.
- Client: The entity that creates and configures commands.
Command Pattern Implementation
Imagine a smart home system, similar to Google Home, where you can control devices like turning lights on/off or playing music. This scenario can be a great example to demonstrate the implementation of the Command design pattern.
// Command.kt
interface Command {
fun execute()
}
Create Receivers
The receiver performs the actual actions. For simplicity, we’ll create two receivers: Light
and MusicPlayer
.
// Light.kt
class Light {
fun turnOn() {
println("Light is turned ON")
}
fun turnOff() {
println("Light is turned OFF")
}
}
// MusicPlayer.kt
class MusicPlayer {
fun playMusic() {
println("Music is now playing")
}
fun stopMusic() {
println("Music is stopped")
}
}
Create Concrete Commands
Each concrete command encapsulates a request to the receiver.
// LightCommands.kt
class TurnOnLightCommand(private val light: Light) : Command {
override fun execute() {
light.turnOn()
}
}
class TurnOffLightCommand(private val light: Light) : Command {
override fun execute() {
light.turnOff()
}
}
// MusicCommands.kt
class PlayMusicCommand(private val musicPlayer: MusicPlayer) : Command {
override fun execute() {
musicPlayer.playMusic()
}
}
class StopMusicCommand(private val musicPlayer: MusicPlayer) : Command {
override fun execute() {
musicPlayer.stopMusic()
}
}
Create the Invoker
The invoker doesn’t know the details of the commands but can execute them. In this case, our remote is the center of home automation and can control everything.
// RemoteControl.kt
class RemoteControl {
private val commands = mutableListOf<Command>()
fun setCommand(command: Command) {
commands.add(command)
}
fun executeCommands() {
commands.forEach { it.execute() }
commands.clear()
}
}
Client Code
Now, let’s create the client code to see the pattern in action.
// Main.kt
fun main() {
// Receivers
val light = Light()
val musicPlayer = MusicPlayer()
// Commands
val turnOnLight = TurnOnLightCommand(light)
val turnOffLight = TurnOffLightCommand(light)
val playMusic = PlayMusicCommand(musicPlayer)
val stopMusic = StopMusicCommand(musicPlayer)
// Invoker
val remoteControl = RemoteControl()
// Set and execute commands
remoteControl.setCommand(turnOnLight)
remoteControl.setCommand(playMusic)
remoteControl.executeCommands() // Executes: Light ON, Music Playing
remoteControl.setCommand(turnOffLight)
remoteControl.setCommand(stopMusic)
remoteControl.executeCommands() // Executes: Light OFF, Music Stopped
}
Here,
Command Interface: The Command
interface ensures uniformity. Whether it’s turning on a light or playing music, all commands implement execute()
.
Receivers: The Light
and MusicPlayer
classes perform the actual work. They are decoupled from the invoker.
Concrete Commands: Each command bridges the invoker and the receiver. This encapsulation allows us to add new commands easily without modifying the existing code (We will see it shortly after this).
Invoker: The RemoteControl
acts as a controller. It queues and executes commands, providing flexibility for batch operations.
Client Code: We bring all components together, creating a functional smart home system.
Enhancing the Pattern
If we wanted to add undo functionality, we could introduce an undo()
method in the Command
interface. Each concrete command would then implement the reversal logic. For example:
interface Command {
fun execute()
fun undo()
}
class TurnOnLightCommand(private val light: Light) : Command {
override fun execute() {
light.turnOn()
}
override fun undo() {
light.turnOff()
}
}
Iterator Design Pattern
To iterate simply means to repeat an action. In software, iteration can be achieved using either recursion or loop structures, like for
and while
loops. When we need to provide functionality for iteration in a class, we often use something called an iterator.
Now, let’s talk about aggregates. Think of an aggregate as a collection of objects. It could be implemented in various forms, such as an array, a vector, or even a binary tree — essentially, any structure that holds multiple objects.
The iterator design pattern offers a structured way to handle how aggregates and their iterators are implemented. This pattern is based on two key design principles:
Separation of Concerns
This principle encourages us to keep different functionalities in separate areas. In the context of iterators, it means splitting the responsibility:
- The aggregate focuses solely on managing (Means storing and organizing) its collection of objects.
- The iterator takes care of traversing through the aggregate.
By doing this, we ensure that the code for maintaining the collection is cleanly separated from the code that deals with traversing it.
Decoupling of Data and Operations
This principle, rooted in generic programming, emphasizes independence between data structures and the operations performed on them. In short, the iterator pattern allows us to create traversal logic that works independently of the underlying data structure — whether it’s an array, a tree, or something else. This makes the traversal code more reusable and adaptable.
In practice, this design pattern simplifies things by moving the traversal logic out of the aggregate and into a dedicated iterator. This way, the aggregate focuses on its core responsibility — managing data — while the iterator focuses on efficiently navigating through that data. By adhering to these principles, we get cleaner, more modular, and reusable code.
Structure of the Iterator Design Pattern
Basically, here:
- Iterator: Defines an interface for accessing and traversing elements.
- Concrete Iterator: Implements the Iterator interface and provides the mechanism for iteration.
- Aggregate: Represents the collection of elements.
- Concrete Aggregate: Implements the collection (Aggregate) interface and returns an iterator to traverse its elements.
Now, let’s implement the Iterator Pattern in Kotlin
Iterator Interface
interface Iterator<T> {
fun first(): T
fun next(): T
fun isDone(): Boolean
fun currentItem(): T
}
Defines the standard methods First()
, Next()
, IsDone()
, and CurrentItem()
.
ConcreteIterator
Implements these methods and provides specific logic for iterating over a list of items.
class ConcreteIterator<T>(private val items: List<T>) : Iterator<T> {
private var currentIndex = 0
override fun first(): T {
return items[0] // Return the first item
}
override fun next(): T {
if (!isDone()) {
return items[currentIndex++] // Move to next and return the current item
}
throw NoSuchElementException("No more items.")
}
override fun isDone(): Boolean {
return currentIndex >= items.size // Check if we've iterated past the last item
}
override fun currentItem(): T {
if (isDone()) throw NoSuchElementException("No more items.")
return items[currentIndex] // Return the current item
}
}
Here,
first()
: Returns the first item in the list.next()
: Returns the next item and increments the index.isDone()
: Checks if all items have been traversed.currentItem()
: Returns the current item.
Aggregate Interface
interface Aggregate<T> {
fun createIterator(): Iterator<T>
}
The Aggregate
interface only defines the createIterator()
method that will return an iterator.
ConcreteAggregate
class ConcreteAggregate<T>(private val items: List<T>) : Aggregate<T> {
override fun createIterator(): Iterator<T> {
return ConcreteIterator(items) // Return a new ConcreteIterator
}
}
The ConcreteAggregate
class implements Aggregate
, and its createIterator()
method returns a new instance of ConcreteIterator
to iterate over the collection.
Client Code
The client creates an aggregate and uses the iterator to traverse the items in the collection.
fun main() {
val books = listOf("Let Us C", "Mastering Kotlin", "Wings of Fire", "Life Lessons")
val bookCollection = ConcreteAggregate(books)
val iterator = bookCollection.createIterator()
println("First item: ${iterator.first()}")
while (!iterator.isDone()) {
println("Current item: ${iterator.currentItem()}")
iterator.next()
}
}
Output
First item: Let Us C
Current item: Let Us C
Current item: Mastering Kotlin
Current item: Wings of Fire
Current item: Life Lessons
Interpreter Design Pattern
The Interpreter design pattern is used to define a representation for a grammar of a language and provide an interpreter that uses the representation to interpret sentences in the language. In simpler terms, it’s a way to evaluate statements or expressions based on a predefined set of rules or grammar.
It’s particularly useful when you need to evaluate strings that follow a specific format, like mathematical expressions, SQL queries, or even programming languages.
Structure of Iterpreter Design Pattern
The main components of the Interpreter pattern:
- AbstractExpression: This defines the interface for all expressions. It usually has an
interpret()
method, which is responsible for interpreting the expression. - TerminalExpression: These are the basic expressions in the grammar. They usually don’t have any sub-expressions. For example, in a mathematical expression, a number or a variable would be a terminal expression.
- NonTerminalExpression: These expressions are made up of one or more terminal or non-terminal expressions. For example, an addition or subtraction operator in a mathematical expression.
- Context: This holds the data or the input we want to interpret.
When Should We Use It?
The Interpreter pattern comes in handy when:
- We need to evaluate a series of expressions that follow some grammar or rules.
- We’re dealing with complex expressions that can be broken down into smaller components.
- The language we’re working with is relatively simple but needs a structured approach.
Now that we know what the pattern is and when to use it, let’s look at how we can implement it in Kotlin.
Wait… Have you ever wanted to create a calculator for math expressions like 3 + 5 - 2
? Or a command parser for a small scripting language? That’s the perfect use case!
Simple Math Expression Interpreter
We’re going to interpret a basic math expression like 3 + (5 - 2)
. Here’s how we’ll do it step by step.
Define the Abstract Expression
We’ll start by defining our abstract expression interface, which will be used by both terminal and non-terminal expressions.
// AbstractExpression interface
interface Expression {
fun interpret(context: Map<String, Int>): Int
}
Here, the interpret
method takes a context
(which can be a map of variable values) and returns an integer result.
Create Terminal Expressions
Now, let’s create terminal expressions. These are the base expressions, like numbers in the expression.
// TerminalExpression class for numbers
class NumberExpression(private val number: Int) : Expression {
override fun interpret(context: Map<String, Int>): Int {
return number
}
}
In this class, we simply store a number, and when we interpret it, we return that number.
Create Non-Terminal Expressions
Next, we’ll implement the non-terminal expressions. These are the operators like addition or subtraction. Each non-terminal expression will hold references to two sub-expressions: the left-hand side and the right-hand side.
// NonTerminalExpression class for addition
class AddExpression(private val left: Expression, private val right: Expression) : Expression {
override fun interpret(context: Map<String, Int>): Int {
return left.interpret(context) + right.interpret(context)
}
}
// NonTerminalExpression class for subtraction
class SubtractExpression(private val left: Expression, private val right: Expression) : Expression {
override fun interpret(context: Map<String, Int>): Int {
return left.interpret(context) - right.interpret(context)
}
}
Here, the AddExpression
and SubtractExpression
are the operators, and they each hold two Expression
objects, representing the left and right operands. When we interpret them, we recursively interpret both sides and then apply the operation. Basically each of these expressions takes two sub-expressions (left and right) and performs an operation on their results.
Build the Expression Tree (Bringing All Together)
Now that we’ve created our expressions, we can evaluate them as a tree, where each node represents an operation and the leaves are the numbers. Let’s explore how these components come together in a simple interpreter.
fun main() {
// Build the expression tree
val expression = AddExpression(
NumberExpression(3),
SubtractExpression(NumberExpression(5), NumberExpression(2))
)
// Create a context if needed (in this case, we don't need it, so we use an empty map)
val result = expression.interpret(emptyMap())
// Print the result
println("Result: $result") // Output will be 3 + (5 - 2) = 6
}
Here,
Expression Tree Construction: To begin, we construct an expression tree. At the root, we have an AddExpression
, which consists of two child nodes:
- The left child is a
NumberExpression(3)
. - The right child is a
SubtractExpression
, which further has two children:NumberExpression(5)
andNumberExpression(2)
.
Interpretation: When the interpret()
method is called on the root node (AddExpression
), it processes its children recursively. The AddExpression
calculates the sum of its left and right sub-expressions. The right sub-expression (SubtractExpression
) computes the result of 5 - 2
. Finally, the root evaluates 3 + 3
, resulting in the value 6
.
Context: In this example, no external variables are required, so we use an empty map as the context. But what if we want to handle variables like x + y
, where the values of x
and y
are defined at runtime? In that case, we would use a context like this:
// Context: x = 3, y = 5
val context = mapOf("x" to 3, "y" to 5)
Observer Design Pattern
The Observer design pattern is used to keep parts of a program in sync. It works by having subjects (the components being watched) notify observers (the components watching) whenever something changes. This creates a system where multiple observers can automatically update themselves when the subject’s state changes. It’s like a group chat where everyone gets notified when someone sends a message, keeping everyone updated.
In the Observer pattern, a subject keeps track of a list of observers and notifies them whenever there’s a change in its state. This is the most common use case, where one subject is observed by many observers.
Observer Design Pattern Structure
The key components of the Observer pattern are:
- Subject: The object that holds the state and notifies observers of changes.
- Observer: The object that wants to be notified about changes in the subject.
- Concrete Subject: A specific implementation of the subject.
- Concrete Observer: A specific implementation of the observer.
Implementation
// Subject Interface
interface Subject {
fun attach(observer: Observer)
fun detach(observer: Observer)
fun notifyObservers()
}
// Observer Interface
interface Observer {
fun update()
}
// Concrete Subject
class ConcreteSubject : Subject {
private val observers = mutableListOf<Observer>()
var state: String = ""
set(value) {
field = value
notifyObservers()
}
// Attach an observer
override fun attach(observer: Observer) {
observers.add(observer)
println("Observer added.")
}
// Detach an observer
override fun detach(observer: Observer) {
observers.remove(observer)
println("Observer removed.")
}
// Notify all observers of a state change
override fun notifyObservers() {
println("Notifying observers...")
observers.forEach { it.update() }
}
}
// Concrete Observer
class ConcreteObserver(private val id: String, private val subject: ConcreteSubject) : Observer {
private var observerState: String = ""
// Update the observer's state
override fun update() {
observerState = subject.state
println("Observer $id state updated to: $observerState")
}
}
// Main function to demonstrate
fun main() {
// Create a concrete subject
val subject = ConcreteSubject()
// Create observers
val observer1 = ConcreteObserver("1", subject)
val observer2 = ConcreteObserver("2", subject)
// Attach observers to the subject
subject.attach(observer1)
subject.attach(observer2)
// Change the subject's state and notify observers
subject.state = "State 1"
subject.state = "State 2"
// Detach an observer and change the state
subject.detach(observer1)
subject.state = "State 3"
}
Output
Observer added.
Observer added.
Notifying observers...
Observer 1 state updated to: State 1
Observer 2 state updated to: State 1
Notifying observers...
Observer 1 state updated to: State 2
Observer 2 state updated to: State 2
Observer removed.
Notifying observers...
Observer 2 state updated to: State 3
Here,
- Both observers (
observer1
andobserver2
) are attached to the subject. - When the state changes to “State 1”, both observers receive the update.
- When the state changes to “State 2”, both observers again receive the update.
observer1
is detached, so onlyobserver2
receives the update when the state changes to “State 3”.
Mediator Design Pattern
The Mediator Design Pattern simplifies communication between multiple objects by introducing a mediator object that acts as a central hub. Instead of objects directly referencing each other, they interact through the mediator. This reduces dependencies and makes the code more modular and easier to manage.
While both the Mediator and Observer patterns involve communication between objects, the key difference is in how they handle it. In the Observer pattern, a subject notifies its observers whenever it changes, leading to direct communication between the subject and its observers. In contrast, the Mediator Pattern centralizes communication, where objects (colleagues) send messages to a mediator instead of directly interacting with each other. The mediator then coordinates and notifies the relevant colleagues about changes.
Think of it like a project manager in a team. Team members don’t communicate directly for every decision; instead, the project manager coordinates their interactions. This reduces chaos and improves collaboration.
When Should We Use the Mediator Design Pattern?
When designing reusable components, tight dependencies between them can lead to tangled, “spaghetti-like” code. In this situation, reusing individual classes becomes difficult because they are too interconnected. It’s like trying to remove one piece from a tangled heap—you either end up taking everything or nothing at all.
Spaghetti Code Analogy: Imagine a string of Christmas lights where each bulb is directly wired to the next. If one bulb is faulty or needs to be replaced, you can’t just swap out that single bulb. Since all the bulbs are tightly connected, replacing one requires adjusting or replacing the entire string. This is similar to spaghetti code, where components are so tightly coupled that isolating one to make changes without affecting others becomes very difficult.
Solution with the Mediator Pattern: Now, imagine instead that each bulb is connected to a central controller (the mediator). If one bulb needs to be replaced or updated, the controller handles the communication between bulbs. The bulbs no longer interact with each other directly. Instead, all communication goes through the mediator. This way, the rest of the system remains unaffected by changes to a single bulb, and the system becomes more modular with fewer dependencies between components.
We should consider using the Mediator Pattern when:
- Multiple objects must interact in complex ways.
- Tight coupling between objects makes the system difficult to maintain or extend.
- Changes in one component should not cascade through the entire system, causing ripple effects.
Structure of Mediator Design Pattern
Mediator Interface
- Defines a contract for communication between components.
Concrete Mediator
- Implements the Mediator interface and manages the communication between components.
Colleague (Component)
- Represents the individual components that interact with each other via the mediator.
Concrete Colleague
- Implements the specific behavior of a component.
The Mediator design pattern is structured to centralize communication and decouple interacting objects. Here’s how it works:
Centralized Communication: All communication between objects (known as colleagues) is routed through a central mediator. This ensures that each object doesn’t need to be aware of the others, and all interactions are coordinated in one place.
Decoupling of Objects: The colleague objects don’t communicate with each other directly. Instead, they send messages through the mediator, which handles the communication. This reduces the complexity of managing direct dependencies between objects. For example, in an air traffic control system, instead of planes communicating directly with one another, they interact with the air traffic controller (the mediator). The controller manages the planes’ interactions, ensuring safe, efficient, and orderly communication, preventing collisions or miscommunication.
Memento Design Pattern
The Memento Design Pattern is one of the behavioral design patterns. It’s all about capturing an object’s state at a particular moment in time, so you can restore it later. It’s like taking a snapshot of an object’s current state and saving it for safekeeping.
Here’s the official definition:
The Memento Design Pattern provides the ability to restore an object to its previous state without exposing the implementation details.
We can’t always expose an object’s internal details due to encapsulation. This pattern allows us to:
- Save the state without breaking encapsulation.
- Support features like undo/redo, checkpoints, or versioning.
Real-Life Analogy
Imagine writing a letter and using an eraser. Before making changes, you take a photo of the letter. If you mess up, you can refer to the photo and restore the original version. In this analogy:
- The letter is the Originator.
- The photo is the Memento.
- You, with your eraser, are the Caretaker.
Memento Design Pattern Structure
Encapsulate an object’s state so that its internal structure is hidden from external entities. The core of the structure lies in the memento, which stores the object’s state, while another object, known as the caretaker, is responsible for saving and restoring the memento without accessing the object’s internal details.
The Memento pattern involves three primary components:
Memento: It’s like a snapshot of an object’s internal state. It saves the object’s data at a specific moment, and it can save only what is necessary.
- Protection: The Memento ensures that only the object that created it (the “Originator”) can access and modify its content. Other objects (like the “Caretaker”) can only store and pass the Memento around without seeing or changing its data.
Originator: This is the object that wants to save its state.
- It creates a Memento to store its current state.
- Later, it can use the Memento to go back to that saved state.
Caretaker: This object is responsible for keeping the Memento safe.
- It never looks inside the Memento or changes its content. It simply stores and retrieves it when needed.
In short, the Originator creates a snapshot (Memento) of its state, and the Caretaker keeps track of these snapshots. Later, the Originator can use the snapshots to restore itself.
In practice, the Originator creates a Memento to store its state, and the Caretaker keeps the Memento for future use. When the user triggers an undo (like pressing Ctrl + Z
), the Caretaker retrieves the saved state from the Memento and hands it back to the Originator, which then reverts to that state.
// Memento class stores the state of the Originator
class Memento(val state: String)
// Originator is the object whose state is saved
class Originator(var state: String) {
// Creates a Memento with the current state
fun createMemento(): Memento {
return Memento(state)
}
// Restores the state from a Memento
fun restore(memento: Memento) {
this.state = memento.state
}
fun showState() {
println("Current State: $state")
}
}
// Caretaker is responsible for storing and restoring the Memento
class Caretaker {
private val mementoList = mutableListOf<Memento>()
// Adds a Memento to the list
fun addMemento(memento: Memento) {
mementoList.add(memento)
}
// Retrieves the last saved Memento (undo functionality)
fun getLastMemento(): Memento? {
if (mementoList.isNotEmpty()) {
return mementoList.removeAt(mementoList.size - 1)
}
return null
}
}
// Demonstrating the Memento pattern in action
fun main() {
val originator = Originator("Initial State")
val caretaker = Caretaker()
originator.showState() // Output: Current State: Initial State
// Save the state
caretaker.addMemento(originator.createMemento())
// Change the state
originator.state = "State 1"
originator.showState() // Output: Current State: State 1
// Save the new state
caretaker.addMemento(originator.createMemento())
// Change the state again
originator.state = "State 2"
originator.showState() // Output: Current State: State 2
// Now let's undo the last state change (Ctrl + Z)
val lastMemento = caretaker.getLastMemento()
if (lastMemento != null) {
originator.restore(lastMemento)
originator.showState() // Output: Current State: State 1
}
// Undo again (Ctrl + Z)
val previousMemento = caretaker.getLastMemento()
if (previousMemento != null) {
originator.restore(previousMemento)
originator.showState() // Output: Current State: Initial State
}
}
Output
Current State: Initial State
Current State: State 1
Current State: State 2
Current State: State 1
Current State: Initial State
Here,
- Initially, the
Originator
has the state “Initial State”. - The state is saved into the
Caretaker
‘s list. - The state changes to “State 1”, and it is saved again.
- The state changes to “State 2”, and the change is saved.
- The
Caretaker
then provides the last saved state (from “State 2” to “State 1”), and then the previous state (“State 1” to “Initial State”) is restored.
Strategy Design Pattern
The Strategy Pattern falls under the category of behavioral design patterns, and its purpose is straightforward: it enables us to define multiple algorithms and switch between them dynamically, without modifying the client code. Instead of duplicating code or repeatedly writing the same logic, this pattern allows you to define a family of algorithms and choose the one that best fits the client’s needs.
This pattern aligns with two key principles of software design:
- Encapsulation: Each algorithm is encapsulated in its own class.
- Open/Closed Principle: The code is open for extension (new strategies can be added) but closed for modification (existing code remains unchanged).
The beauty of the Strategy Pattern lies in its simplicity and flexibility. It enables you to add new features or extend functionality without requiring significant changes to existing code. Additionally, it allows your program to swap behaviors dynamically at runtime, making it highly adaptable to changing requirements with minimal effort.
When to Use the Strategy Pattern?
You should consider using the Strategy Pattern when:
- You have multiple ways to accomplish a task and want the flexibility to switch between them easily.
- You want to avoid cluttered classes with lots of
if
orwhen
statements. - You need to keep the algorithm’s implementation separate from the rest of your code.
- You want your code to be easily extended with new features or behaviors without changing existing code.
Here are a few real-life scenarios where the Strategy Pattern works really well:
- Payment gateways: Letting users choose between different payment methods, like credit cards, PayPal, or bank transfers.
- Sorting algorithms: Allowing users to switch between sorting methods, like quick sort or bubble sort, based on their preference.
- Discount calculations: Handling various types of discounts in a shopping cart, such as percentage-based, fixed amount, or special promotions.
We will soon look at the code implementation, but before that, let’s first understand the structure of the Strategy Pattern and its key components.
Strategy Design Pattern Structure
Let’s break the pattern into its core components:
Strategy
- Defines a common interface for all the supported algorithms.
- The context uses this interface to call the algorithm defined by a specific strategy.
ConcreteStrategy
- Implements the algorithm as outlined by the Strategy interface.
Context
- Is configured with a ConcreteStrategy object.
- Holds a reference to a Strategy object.
- May offer an interface that allows the Strategy to access its internal data or state, but only when necessary.
Let’s now implement the Strategy Design Pattern for a simple calculation operation.
Create an Interface
interface Strategy {
fun doOperation(num1: Int, num2: Int): Int
}
Create Concrete Classes Implementing the Interface
class OperationAdd : Strategy {
override fun doOperation(num1: Int, num2: Int): Int {
return num1 + num2
}
}
class OperationSubtract : Strategy {
override fun doOperation(num1: Int, num2: Int): Int {
return num1 - num2
}
}
class OperationMultiply : Strategy {
override fun doOperation(num1: Int, num2: Int): Int {
return num1 * num2
}
}
Create Context Class
class Context(private var strategy: Strategy) {
fun executeStrategy(num1: Int, num2: Int): Int {
return strategy.doOperation(num1, num2)
}
}
Use the Context to See Change in Behaviour When It Changes Its Strategy
fun main() {
var context = Context(OperationAdd())
println("10 + 5 = ${context.executeStrategy(10, 5)}")
context = Context(OperationSubtract())
println("10 - 5 = ${context.executeStrategy(10, 5)}")
context = Context(OperationMultiply())
println("10 * 5 = ${context.executeStrategy(10, 5)}")
}
Output
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
Here,
- The
Strategy
interface defines the contract for the algorithm. OperationAdd
,OperationSubtract
, andOperationMultiply
are concrete classes that implement theStrategy
interface.- The
Context
class uses aStrategy
to perform an operation. - In
main()
, we change theStrategy
at runtime by passing differentStrategy
objects to theContext
.
Template Method Pattern
The Template Method Pattern is a behavioral design pattern that defines the skeleton of an algorithm in a base class and lets subclasses override specific steps without altering the algorithm’s overall structure.
Think of it as a recipe. You can follow the recipe to make a dish, but certain ingredients or techniques might vary depending on your preferences. The structure remains the same, but you get the flexibility to tweak the details.
Let me simplify that. Imagine you’re a chef creating a recipe for your restaurant. Some steps, like washing ingredients and plating the dish, are always the same. But steps like seasoning or cooking style might vary depending on the type of dish. The Template Method allows you to define this recipe in a general way while letting individual chefs (subclasses) tweak specific steps.
Template Method Pattern: Problem and Solution
Understanding the Problem: Imagine you are building a gaming application with different types of games: Chess and Soccer. Each game has the following steps:
- Initialize the game.
- Start playing.
- End the game.
While the general structure is the same, the details of each step vary for Chess and Soccer. If you were to write separate implementations for each game, you might duplicate code for the steps that are common, which violates the DRY principle (Don’t Repeat Yourself).
Solution: The Template Method pattern addresses this by providing a template (skeleton) for the algorithm in a base class. Subclasses define the specific behavior for the varying steps.
Key Principles of the Template Method Pattern
- Algorithm Structure: The base class provides a high-level structure of the algorithm.
- Customizable Steps: Subclasses implement the specific parts of the algorithm.
- Consistency and Reusability: Common steps are reused, ensuring consistency across implementations.
Structure of Template Method Pattern
Key Components of the Template Method Pattern
- Abstract Class: Contains the template method, which defines the algorithm’s structure and some default implementations of steps.
- Template Method: A method that defines the sequence of steps in the algorithm. Some steps are concrete (already implemented), while others are abstract (to be implemented by subclasses).
- Concrete Class: Implements the abstract steps to provide specific behaviors.
Think of an abstract class as the foundation for a group of related classes. The common behavior for all these classes is defined in the abstract class, while the specific details are handled by the individual subclasses. The Template Method pattern gives you a way to outline the basic structure of an algorithm in a method, while leaving certain steps for the subclasses to fill in. This lets subclasses customize parts of the algorithm without changing its overall structure.
Let’s implement the Template Method design pattern for our gaming application.
Define the Abstract Class
abstract class Game {
// Template method
fun play() {
initialize()
startPlay()
endPlay()
}
// Steps of the algorithm (template method components)
abstract fun initialize()
abstract fun startPlay()
abstract fun endPlay()
}
- The
play()
method is the template method. It defines the skeleton of the algorithm. - The steps
initialize()
,startPlay()
, andendPlay()
are abstract and must be implemented by subclasses.
Create Concrete Classes
Chess Game
class Chess : Game() {
override fun initialize() {
println("Chess Game Initialized. Set up the board.")
}
override fun startPlay() {
println("Chess Game Started. Players are thinking about their moves.")
}
override fun endPlay() {
println("Chess Game Finished. Checkmate!")
}
}
Soccer Game
class Soccer : Game() {
override fun initialize() {
println("Soccer Game Initialized. Players are on the field.")
}
override fun startPlay() {
println("Soccer Game Started. Kickoff!")
}
override fun endPlay() {
println("Soccer Game Finished. The final whistle blows.")
}
}
Use the Template Method
fun main() {
println("Playing Chess:")
val chess = Chess()
chess.play()
println("\nPlaying Soccer:")
val soccer = Soccer()
soccer.play()
}
Output
Playing Chess:
Chess Game Initialized. Set up the board.
Chess Game Started. Players are thinking about their moves.
Chess Game Finished. Checkmate!
Playing Soccer:
Soccer Game Initialized. Players are on the field.
Soccer Game Started. Kickoff!
Soccer Game Finished. The final whistle blows.
Here,
- Abstract Class:
- The
Game
class encapsulates the skeleton of the algorithm in theplay()
method. - The
play()
method ensures the steps are executed in the defined order.
- The
- Concrete Classes:
- The
Chess
andSoccer
classes override the abstract methods to provide specific implementations for each step.
- The
- Reusability:
- The
play()
method in theGame
class ensures the overall structure of the algorithm is consistent across different games.
- The
The Secret Twist of the Hook Method
In the Template Method Pattern, the Hook Method is an optional concept that adds flexibility to the algorithm. The Hook Method is a method defined in the abstract class, but it doesn’t have to do anything by default. Instead, it provides a “hook” for subclasses to override and implement custom behavior, if needed, without changing the overall flow of the algorithm.
Note: A hook is a simple method (not marked with any special ‘hook’ keyword; it’s just a regular method with hook functionality) defined in the abstract class, typically with an empty or default implementation. It enables subclasses to ‘hook into’ the algorithm at specific points if needed. Subclasses can also choose to ignore the hook if it isn’t relevant to their specific behavior.
How it works:
- The Template Method defines the skeleton of an algorithm, calling a series of steps (methods), some of which can be abstract (requiring subclasses to implement them).
- A Hook Method is a method in the abstract class that does nothing by default but can be overridden in the subclasses to add specific functionality.
Why it’s useful:
- Flexibility: It allows subclasses to optionally customize parts of the algorithm without changing the structure.
- Control: The base class controls the algorithm’s flow, while allowing subclasses to “hook” in additional behavior when needed.
abstract class CookingRecipe {
fun cook() {
prepareIngredients()
cookMainPart()
serve()
}
// Must be implemented by subclasses
abstract fun prepareIngredients()
// Default implementation, can be overridden
open fun cookMainPart() {
println("Cooking the main dish in a standard way")
}
// Hook method
open fun serve() {
println("Serving the dish")
}
}
class PastaRecipe : CookingRecipe() {
override fun prepareIngredients() {
println("Preparing pasta, sauce, and vegetables")
}
override fun cookMainPart() {
println("Cooking the pasta and sauce together")
}
// Override hook method to add custom behavior
override fun serve() {
println("Serving the pasta with extra cheese")
}
}
Here,
prepareIngredients()
is an abstract method, so subclasses must implement it.cookMainPart()
has a default implementation that can be overridden.serve()
is a hook method. It has a default behavior but can be overridden in subclasses to provide custom serving logic.
Visitor Design Pattern
The Visitor Design Pattern is a behavioral design pattern. Its primary goal is to separate an algorithm from the object structure on which it operates. In short, it allows you to add new operations (or behaviors) to a set of classes without modifying their source code.
Imagine this: You’ve just arrived in a new city and you’re super excited to explore. You quickly hop onto Google and search for “must-visit places near me.” The results are full of options: a beautiful park with breathtaking views, a cozy restaurant that serves your favorite cuisine, and a renowned museum with tons of fascinating exhibits. You decide to visit all of them.
The next morning, you set out. First, you head to the park and take a bunch of photos because, well, it’s stunning. Then, you go to the restaurant and indulge in that delicious dish you’ve been craving. Finally, you make your way to the museum, grab a ticket, and check out some of the exhibits before catching a movie in the cinema hall.
Each place is different, and at each one, you do something unique based on what it is — take photos, eat, or watch a movie. But here’s the key: you didn’t change the park, the restaurant, or the museum. You simply experienced them in different ways.
This is where the Visitor Design Pattern comes into play.
In short, the Visitor Design Pattern is all about interacting with different objects (in this case, the park, restaurant, and museum) in different ways, but without changing the objects themselves. It’s like you, as the “visitor,” can do different activities based on the type of place (object) you’re visiting, but the places (objects) remain exactly the same.
How Does It Work?
In programming, this pattern allows you to add new operations (or behaviors) to objects without changing their internal code. This is important because often, we don’t want to go digging into existing code and modifying it when we want to add new features. Instead, we can add new operations externally.
This pattern involves two main components:
- Visitor: Encapsulates the new operation you want to perform.
- Visitable or Element: Represents the classes that the visitor will operate on.
The Visitor Design Pattern mainly relies on the double dispatching mechanism. To grasp double dispatching, it’s important to first understand single dispatching.
Single Dispatch
Most design patterns that use delegation rely on a feature called single dispatch. This means:
- The specific function that gets called is determined by the type of one object (the object the method is being called on).
- This is also known as a virtual function call, where the dynamic type (the actual type of the object at runtime) decides which function to execute.
In languages like Java or Kotlin, if you have a method in a superclass that is overridden in a subclass, the method that gets executed is determined at runtime based on the actual object type, not the reference type. This behavior, known as polymorphism, is an example of single dispatch because the method is chosen based on the type of a single object (the one calling the method).
Double Dispatch
The Visitor Design Pattern extends this concept by introducing double dispatch, where the method that gets called depends on two factors:
- The method being invoked
- The runtime types of two objects (e.g., the object being visited and the visitor object).
This means:
- Double dispatch is a mechanism where the function call depends on the runtime types of two objects instead of one.
- In the Visitor Design Pattern, double dispatch ensures that the correct function is executed based on the combination of the Visitor and the element being visited.
So, final thoughts on them:
- Single Dispatch: The method invoked depends on the method name and the type of the receiver (polymorphism).
- Double Dispatch: The method invoked depends on the method name and the types of two receivers (the element and the visitor). This allows adding operations to an object structure without modifying the structure itself.
Structure of the Visitor Design Pattern
There are five main players in this pattern:
Visitor
- An interface or abstract class defining operations to be performed on elements (Visitable objects) in the structure.
- Declares methods for performing specific operations on each type of element in the structure.
interface Visitor {
fun visitConcreteElementA(element: ConcreteElementA)
fun visitConcreteElementB(element: ConcreteElementB)
}
Concrete Visitor
- Implements the
Visitor
interface. - Defines specific behaviors for each type of element it visits.
- May maintain state or data relevant to the operations performed during visitation.
class ConcreteVisitor : Visitor {
override fun visitConcreteElementA(element: ConcreteElementA) {
// Specific operation for ConcreteElementA
}
override fun visitConcreteElementB(element: ConcreteElementB) {
// Specific operation for ConcreteElementB
}
}
Visitable (Element)
- An interface or abstract class representing elements that can be visited by a
Visitor
. - Declares an
accept(visitor: Visitor)
method, enabling the element to accept aVisitor
and let it perform its operation.
interface Visitable {
fun accept(visitor: Visitor)
}
Concrete Visitable Classes
- Implements the
Visitable
interface. - Provides the
accept(visitor: Visitor)
method implementation, which calls the appropriatevisit()
method on the visitor, passing itself as a parameter.
class ConcreteElementA : Visitable {
override fun accept(visitor: Visitor) {
visitor.visitConcreteElementA(this)
}
}
class ConcreteElementB : Visitable {
override fun accept(visitor: Visitor) {
visitor.visitConcreteElementB(this)
}
}
Object Structure
- A container (e.g., array, list, or set) that holds the elements to be visited.
- Provides a way to traverse its elements and allows a
Visitor
to access and operate on each element.
class ObjectStructure(private val elements: List<Visitable>) {
fun accept(visitor: Visitor) {
elements.forEach { it.accept(visitor) }
}
}
In short, here’s how they work together:
- The Visitor defines operations for each element type.
- The ConcreteVisitor implements those operations.
- Elements (via
accept()
) allow the Visitor to perform operations on them without modifying their code. - The ObjectStructure organizes and manages the elements, letting the Visitor traverse and interact with them.
Let’s go further and implement a simple example using the Visitor design pattern to handle different types of shapes, such as Circle and Rectangle.
Define the Visitor interface: This will declare the visit()
method for each concrete shape.
// Visitor interface
interface ShapeVisitor {
fun visit(circle: Circle)
fun visit(rectangle: Rectangle)
}
Create ConcreteVisitor: We’ll define a concrete visitor that performs an operation on the shapes.
// ConcreteVisitor
class AreaCalculator : ShapeVisitor {
override fun visit(circle: Circle) {
println("Calculating area of circle: π * ${circle.radius}^2")
val area = Math.PI * circle.radius * circle.radius
println("Area: $area")
}
override fun visit(rectangle: Rectangle) {
println("Calculating area of rectangle: ${rectangle.width} * ${rectangle.height}")
val area = rectangle.width * rectangle.height
println("Area: $area")
}
}
Define the Visitable interface: This will declare the accept()
method, which allows a visitor to visit the object.
// Visitable interface
interface Shape {
fun accept(visitor: ShapeVisitor)
}
Create ConcreteVisitable classes: These are concrete shapes like Circle
and Rectangle
that implement the Shape
interface and define the accept()
method.
// ConcreteVisitable classes
class Circle(val radius: Double) : Shape {
override fun accept(visitor: ShapeVisitor) {
visitor.visit(this)
}
}
class Rectangle(val width: Double, val height: Double) : Shape {
override fun accept(visitor: ShapeVisitor) {
visitor.visit(this)
}
}
Client code: This is where we use the visitor pattern. We create the shapes and use the AreaCalculator
to perform operations.
fun main() {
val shapes: List<Shape> = listOf(Circle(5.0), Rectangle(4.0, 6.0))
// Create the visitor
val areaCalculator = AreaCalculator()
// Visit each shape and calculate the area
shapes.forEach { shape ->
shape.accept(areaCalculator)
}
}
//////////// OUTPUT //////////////////
Calculating area of circle: π * 5.0^2
Area: 78.53981633974483
Calculating area of rectangle: 4.0 * 6.0
Area: 24.0
Now, you might be thinking, ‘What is the benefit of doing this?‘ Yes, there are several:
- Open for extension, closed for modification: You can add new operations (visitors) without modifying the existing code for shapes.
- Separation of concerns: The logic for operations is kept separate from the objects, making it easier to manage and extend.
In this way, the Visitor pattern helps you add new functionalities to existing classes without changing their code. It’s especially useful when you need to perform different operations on a group of objects that share a common interface.
Conclusion
Behavioral design patterns enhance the interaction between objects by clearly defining the roles and responsibilities of each component. The patterns we’ve covered today, including Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, and Visitor, represent just a small selection of the many behavioral patterns available.
By incorporating these patterns into your Kotlin projects, you can make your code more flexible, maintainable, and easier to modify as requirements evolve. As with any skill, mastering design patterns takes time and practice, so experiment with these patterns in your own work to build a deeper understanding.
Happy Coding..!