Reversing words in a sentence is a common programming task often used in coding interviews and algorithm-based challenges. In this blog, weโll break down a Kotlin function that reverses the order of words in a given string. Weโll analyze each part of the code, identify potential improvements, and present an optimized version.
Reversing Words: Problem Statement
Given an input string containing multiple words separated by spaces, we need to reverse the order of words while maintaining their original form.
Input:
Kotlin
"Hello India Pune World"
Output:
Kotlin
"World Pune India Hello"
Understanding the Kotlinย Code
Letโs analyze the following Kotlin function that reverses the order of words in a string:
Understand how lists workโโโmutable and immutable lists impact how you modify data in Kotlin.
Code readability is importantโโโthe optimized version is much easier to understand and maintain.
Conclusion
Reversing words in a string is a simple yet insightful exercise in Kotlin. The initial approach using a loop and swapping elements works but is not the most efficient solution. By leveraging Kotlinโs built-in functions, we can achieve the same result with cleaner and more readable code.
Understanding such basic transformations is crucial for improving problem-solving skills, especially in coding interviews and real-world applications.
If youโre learning Kotlin and want to understand how data structures work, linked lists are a fundamental concept worth mastering. A linked list is a collection of values arranged in a linear, unidirectional sequence. Compared to contiguous storage options like arrays, linked lists offer several theoretical advantages, such as constant-time insertion and removal from the front of the list, along with other reliable performance characteristics.
In this blog, weโll cover everything you need to know about linked lists in Kotlin โ including what they are, how they work, and how to implement them, with clear explanations and code examples.
What is a Linked List?
A Linked List is a data structure consisting of a sequence of elements, called nodes.
Each node has two components:
Data: The value we want to store.
Next: A reference to the next node in the sequence.
Unlike arrays, Linked Lists are dynamic in size, offering efficient insertions and deletions at any position in the list.
In a linked list, each node stores a value and points to the next node in the chain. The last node in the sequence points to โnull,โ indicating the end of the list.
Linked lists have several advantages over arrays or ArrayLists in Kotlin:
Quick insertions and removals at the front of the list.
Consistent performance for operations, especially for inserting or removing elements anywhere in the list.
Types of Linked Lists
Singly Linked List โ Each node points to the next node in the sequence (weโll focus on this one & only for insertion operations).
Doubly Linked List โ Each node has a reference to both the next and the previous node.
Circular Linked List โ The last node points back to the first node, forming a loop.
Why Use Linked Lists?
Before we dive into the code, letโs understand why we might choose Linked Lists over arrays:
Dynamic Size โ No need to specify a fixed size upfront.
Efficient Insertions/Deletions โ Adding or removing elements doesnโt require shifting other elements.
Memory Efficiency โ Uses only as much memory as needed.
However, Linked Lists have their trade-offs. Accessing elements is slower compared to arrays because you canโt directly access an element by an index โ you have to traverse the list.
Building a Singly Linked List in Kotlin
Now that we understand what Linked Lists are, letโs build one step-by-step in Kotlin! Weโll create the following:
Node Class โ Represents each element in the list.
LinkedList Class โ Manages the nodes and provides functionality to add, remove, and display elements.
Defining the Node Class
Each node needs to store data and a reference to the next node. Hereโs our Node class:
Kotlin
// We define a node of the linked list as a data class, where it holds a value and a reference to the next node.dataclassNode<T>(varvalue: T, var next: Node<T>? = null) {overridefuntoString(): String {returnif (next != null) {"$value -> ${next.toString()}" } else {"$value" } }}funmain() {val node1 = Node(value = 1)val node2 = Node(value = 2)val node3 = Node(value = 3) node1.next = node2 node2.next = node3 //here node3 points to null at last, as per our code we only print its valueprintln(node1)}//OUTPUT1->2->3
Here, we defined a generic Node class for a linked list in Kotlin. Each Node holds a value of any type (T) and a reference to the next Node, which can be null. The toString() method provides a custom string representation for the node, recursively displaying the value of the node followed by the values of subsequent nodes, separated by ->. If the node is the last in the list, it simply shows its value.
Have you observed how we constructed the list above? We essentially created a chain of nodes by linking their โnextโ references. However, building lists in this manner becomes impractical as the list grows larger. To address this, we can use a LinkedList, which simplifies managing the nodes and makes the list easier to work with. Letโs explore how we can implement this in Kotlin.
Creating the LinkedList Class
Letโs create our LinkedList class and add core functionalities like adding nodes and displaying the list.
Basically, a linked list has a โheadโ (the first node) and a โtailโ (the last node). In a singly linked list, we usually only deal with the head node, although the tail node can also be relevant, especially when adding elements at the end. The tail node becomes more important in doubly linked lists or circular linked lists, where it supports bidirectional traversal or maintains circular references. However, here, we will use both nodes in a singly linked list.
Here, a linked list has a โheadโ (the first node) and a โtailโ (the last node). Weโll also store the listโs size in a โsizeโ property.
Adding values to the list
Now, letโs develop a way to manage the nodes in our list and focus on adding values. There are three common approaches to inserting values into a linked list, each offering different performance benefits:
Push: Inserts a value at the start of the list.
Append: Adds a value to the end of the list.
Insert: Places a value after a specified node in the list.
Weโll implement these methods step by step and analyze their performance.
Push Operations
Inserting a value at the beginning of the list is called a push operation or head-first insertion. The implementation for this is remarkably straightforward.
To achieve this, add the following method to our LinkedList class:
Kotlin
// This function is used to insert a new element at the first position in the linked list.// This is a head-first insertion.funpushAtHead(value: T) { head = Node(value, next = head) // The previous head value (i.e., null if the list is empty) is assigned to the next node.// If the list is empty (i.e., tail is null), we add the new node and assign it to the tail.// If the tail is not null, we add the new element and assign it to the head (as done above).if (tail == null) { tail = head } size++ // Whenever a new node is added, the size is increased by one.}
What happens here is that when we push into an empty list, the new node becomes both the head and the tail. Since the list now contains one node, we increase the size.
We will run this code, but I have a question for you: Should we always run this code in the main function? I mean, should we always copy and paste it into Android Studio or Kotlin Playground? What if, in the future, we want to revisit or refer to it? The answer is simpleโwe can use special Kotlin features like infix functions and higher-order functions. By doing this, we can create a Runner class and add different functionalities using infix and higher-order functions. This approach will make the code easier to understand and manage in the future. Without further delay, letโs implement it, and Iโll explain how it works.
First of all, create a new file and name it LinkedListRunner.kt. Then, add the following code:
Kotlin
funmain() {// Push operation at the first position in the linked list."push"example {val list = LinkedList<String>() list.pushAtHead("amol") list.pushAtHead("satara") list.pushAtHead("bajirao") list.pushAtHead("pune")println(list) }}// This infix function is used to print an example description and then execute the provided function.infixfunString.example(function: () -> Unit) {println("----- Example of $this -----")function()println()}
Here,
Infix Function (โpushโ example{..})
An infix function allows you to call a function in a cleaner and more readable way, without parentheses and dots. In this case, "push" example {...} is an example of an infix function.
How it works:
"push" is a string, and when combined with example, it invokes the example function.
The infix keyword enables the usage of this function in a special syntax: a infixFun b instead of a.infixFun(b).
In this function, this refers to the string "push" and is used to print a custom message (like "----- Example of push -----").
Higher-Order Function (String.example())
The example function is also a higher-order function because it takes another function as a parameter.
How it works:
The example function takes a lambda expression (function: () -> Unit) as a parameter. This is a function that does not take any arguments and returns nothing (Unit is equivalent to void).
Inside example, the function passed (function()) is executed.
In your code, the block of code inside {} (which modifies the linked list) is the function being passed to example as a lambda.
So, finally,ย
Infix Function: The example function is used with infix notation, making the code more readable. It prints the description "----- Example of push -----" and then runs the provided code block.
Higher-Order Function: The example function is a higher-order function because it accepts a lambda function as a parameter and executes it inside its body.
Execution Flow will beโฆ,
The string "push" calls the infix function example.
The block of code inside {} (which adds items to the linked list) is passed as a lambda to the example function.
example prints the message "----- Example of push -----", runs the lambda function to manipulate the linked list, and prints the result.
Kotlin
----- Example of push -----pune -> bajirao -> satara -> amol
This approach is good, but we can improve it further. By using the fluent interface pattern, you can chain multiple push() calls together. To do this, modify the push() method to return LinkedList<T>. Then, add return this at the end, so it returns the list after adding the new element.
Create a new method in LinkedList.kt. The method should now look like this:
Kotlin
// Push operation using chaining.// Head-first insertion using chaining.funpushingAtHead(value: T): LinkedList<T> { head = Node(value, next = head) // The previous head value is assigned to the next node.// If the list is empty (i.e., tail is null), assign the new node to the tail.if (tail == null) { tail = head } size++ // Increment the size of the list whenever a new node is added.returnthis// Return the updated list to enable chaining.}
In the main() function of the LinkedListRunner.kt file, you can either create a new method or update the existing one to use the return value of pushAtHead().
Kotlin
"fluent interface for chain pushing" example {val list = LinkedList<Int>() list.pushingAtHead(2).pushingAtHead(3).pushingAtHead(7).pushingAtHead(1)println(list) }//Output ----- Example of fluent interfacechain pushing -----1->7->3->2
Thatโs better..! Now, you can easily add multiple elements to the beginning of the list.
Wait a momentโฆ! What we see here might seem unclear at first, but is it really necessary? Yes, it is! Thatโs why Iโve covered it here. As we go further, youโll get used to it, and things will become much clearer.
Append Operations
Next, weโll focus on the append operation, which adds a value to the end of the list (also known as tail-end insertion).
In LinkedList.kt, weโll add the following code right below the push() method:
Kotlin
// Append a new node at the last position of the linked list.// Tail-end insertion.funappendAtTail(value: T) {// Example: 1 -> 2 -> 3 -> 4if (isEmpty()) {pushAtHead(value) // If the list is empty, add the new node at the head.return }// If the list is not empty, link the new node to the tail and update the tail. tail?.next = Node(value) tail = tail?.next // Update the tail to the newly added node. size++ // Increment the size of the list when a new node is added.}
This code is quite simple:
As before, if the list is empty, we need to set both the head and the tail to the new node. Since appending to an empty list is the same as pushing, we can use the push method to handle this for us.
For all other cases, we create a new node after the current tail node. The tail wonโt be null here because weโve already handled the empty list case earlier in the code.
Since this is a tail-end insertion, the new node becomes the tail of the list.
Now, go back to LinkedListRunner.kt and add the following code at the bottom of main():
Kotlin
"append"example{val list = LinkedList<Int>() list.appendAtTail(1) list.appendAtTail(2) list.appendAtTail(3) list.appendAtTail(4)println(list) }//Output ----- Example of append -----1->2->3->4
You can apply the technique you used for push() to create a fluent interface for append() as well.
Kotlin
// Append using chaining.// Tail-end insertion using chaining.funappendingAtTail(value: T): LinkedList<T> {if (isEmpty()) {pushAtHead(value) // If the list is empty, add the new node at the head.returnthis }// If the list is not empty, link the new node to the tail and update the tail. tail?.next = Node(value) tail = tail?.next // Update the tail to the newly added node.returnthis// Return the updated list to enable chaining.}
Whether you find it useful or not, just think about the possibilities of chaining both push and append together. Or, feel free to have some fun experimenting with it.
Insert Operations
The third and final operation for adding values is insert(afterNode: Node<T>). This operation allows us to insert a value at a specific position in the list, and it involves two steps:
Locating the node where the value should be inserted.
Inserting the new node right after the located node.
First, weโll write the code to find the node where we want to insert the value.
In LinkedList.kt, add the following code just below the append() method:
Kotlin
// Get the node at the specified index.funnodeAt(index: Int): Node<T>? {var currentNode = headvar currentIndex = 0// Traverse the list until the node at the specified index is found.while (currentNode != null && currentIndex < index) { currentNode = currentNode.next currentIndex++ }return currentNode // Return the node at the specified index, or null if not found.}
nodeAt() retrieves a node from the list based on the given index. Since we can only start from the head node, we need to traverse the list step by step. Hereโs how it works:
We start with a reference to the head node and keep track of the number of steps taken.
Using a while loop, we move through the list until we reach the desired index. If the list is empty or the index is out of range, it will return null.
Next, we will insert the new node. To do this, add the following method below nodeAt().
Kotlin
// Insert a new node with the specified value after the given node.// If the node is the tail, the new node is appended at the tail.funinsertAt(value: T, afterNode: Node<T>): Node<T>? {if (tail == afterNode) {appendAtTail(value) // If afterNode is the tail, append the new node at the tail.return tail!! // Return the updated tail. }// Create a new node and link it to the node after the given node.val newNode = Node(value = value, next = afterNode.next) afterNode.next = newNode // Update the next pointer of the afterNode to the new node. size++ // Increment the size of the list.return newNode // Return the newly inserted node.}
Hereโs what weโve done:
If the method is called with the tail node, we use the append method, which updates the tail.
Otherwise, we create a new node and link it to the next node in the list.
We update the specified node to point to the new node.
To test this, go to LinkedListRunner.kt and add the following code at the bottom of main().
Kotlin
"linked list insert At perticular index "example {val list = LinkedList<Int>() list.pushAtHead(1) list.pushAtHead(2) list.pushAtHead(3)println("list before insert $list")var middleNode = list.nodeAt(1)!!for(i in1..3){ middleNode = list.insertAt(-1 * i, middleNode)!! }println("After inserting $list") }//Output ----- Example of linked list insert At perticular index ----- list before insert 3->2->1 After inserting 3->2-> -1-> -2-> -3->1
Great job! Youโve made excellent progress. To recap, youโve implemented the three operations for adding values to a linked list, as well as a method to find a node at a specific index.
Conclusion
Weโve explored the key insertion operations in linked lists, along with the foundational concepts and structure that make them an essential part of data management. Understanding these operations provides a solid base for working with linked lists in various scenarios. As you continue to practice, youโll gain more proficiency in implementing and manipulating linked lists, further enhancing your problem-solving skills in Kotlin.
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
Structure of the Chain of Responsibility
Handler (Abstract Class or Interface)
Defines the interface for handling requests and the reference to the next handler in the chain.
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.
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.
Kotlin
funmain() {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 or when 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.
Kotlin
interfaceState {funhandle(context: Context)}
Here,
The State interface declares a single method, handle(context: Context), which the Context 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.
Kotlin
classConcreteStateA : State {overridefunhandle(context: Context) {println("State A: Handling request and transitioning to State B") context.setState(ConcreteStateB()) // Transition to State B }}classConcreteStateB : State {overridefunhandle(context: Context) {println("State B: Handling request and transitioning to State A") context.setState(ConcreteStateA()) // Transition to State A }}
ConcreteStateA and ConcreteStateB implement the State 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.
Kotlin
classContext {privatevar currentState: State? = nullfunsetState(state: State) { currentState = stateprintln("Context: State changed to ${state::class.simpleName}") }funrequest() { currentState?.handle(this) ?: println("Context: No state is set") }}
The Context class holds a reference to the current state via currentState.
The setState() method updates the current state and logs the transition.
The request() method delegates the action to the current stateโs handle() method.
Test the Implementation
Finally, we can create a main function to test the transitions between states.
Kotlin
funmain() {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}
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.
Kotlin
// Command.ktinterfaceCommand {funexecute()}
Create Receivers
The receiver performs the actual actions. For simplicity, weโll create two receivers:ย Lightย andย MusicPlayer.
Kotlin
// Light.ktclassLight {funturnOn() {println("Light is turned ON") }funturnOff() {println("Light is turned OFF") }}// MusicPlayer.ktclassMusicPlayer {funplayMusic() {println("Music is now playing") }funstopMusic() {println("Music is stopped") }}
Create Concrete Commands
Each concrete command encapsulates a request to the receiver.
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.
Now, letโs create the client code to see the pattern in action.
Kotlin
// Main.ktfunmain() {// Receiversval light = Light()val musicPlayer = MusicPlayer()// Commandsval turnOnLight = TurnOnLightCommand(light)val turnOffLight = TurnOffLightCommand(light)val playMusic = PlayMusicCommand(musicPlayer)val stopMusic = StopMusicCommand(musicPlayer)// Invokerval 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:
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
Defines the standard methods First(), Next(), IsDone(), and CurrentItem().
ConcreteIterator
Implements these methods and provides specific logic for iterating over a list of items.
Kotlin
classConcreteIterator<T>(privateval items: List<T>) : Iterator<T> {privatevar currentIndex = 0overridefunfirst(): T {return items[0] // Return the first item }overridefunnext(): T {if (!isDone()) {return items[currentIndex++] // Move to next and return the current item }throwNoSuchElementException("No more items.") }overridefunisDone(): Boolean {return currentIndex >= items.size // Check if we've iterated past the last item }overridefuncurrentItem(): T {if (isDone()) throwNoSuchElementException("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.
The Aggregate interface only defines the createIterator() method that will return an iterator.
ConcreteAggregate
Kotlin
classConcreteAggregate<T>(privateval items: List<T>) : Aggregate<T> {overridefuncreateIterator(): Iterator<T> {returnConcreteIterator(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.
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.
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.
Kotlin
// TerminalExpression class for numbersclassNumberExpression(privateval number: Int) : Expression {overridefuninterpret(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.
Kotlin
// NonTerminalExpression class for additionclassAddExpression(privateval left: Expression, privateval right: Expression) : Expression {overridefuninterpret(context: Map<String, Int>): Int {return left.interpret(context) + right.interpret(context) }}// NonTerminalExpression class for subtractionclassSubtractExpression(privateval left: Expression, privateval right: Expression) : Expression {overridefuninterpret(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.
Kotlin
funmain() {// Build the expression treeval 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 resultprintln("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) and NumberExpression(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:
Kotlin
// Context: x = 3, y = 5val 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
Kotlin
// Subject InterfaceinterfaceSubject {funattach(observer: Observer)fundetach(observer: Observer)funnotifyObservers()}// Observer InterfaceinterfaceObserver {funupdate()}// Concrete SubjectclassConcreteSubject : Subject {privateval observers = mutableListOf<Observer>()var state: String = ""set(value) {field = valuenotifyObservers() }// Attach an observeroverridefunattach(observer: Observer) { observers.add(observer)println("Observer added.") }// Detach an observeroverridefundetach(observer: Observer) { observers.remove(observer)println("Observer removed.") }// Notify all observers of a state changeoverridefunnotifyObservers() {println("Notifying observers...") observers.forEach { it.update() } }}// Concrete ObserverclassConcreteObserver(privateval id: String, privateval subject: ConcreteSubject) : Observer {privatevar observerState: String = ""// Update the observer's stateoverridefunupdate() { observerState = subject.stateprintln("Observer $id state updated to: $observerState") }}// Main function to demonstratefunmain() {// Create a concrete subjectval subject = ConcreteSubject()// Create observersval 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
Kotlin
Observer added.Observer added.Notifying observers...Observer 1 state updated to: State1Observer 2 state updated to: State1Notifying observers...Observer 1 state updated to: State2Observer 2 state updated to: State2Observer removed.Notifying observers...Observer 2 state updated to: State3
Here,
Both observers (observer1 and observer2) 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 only observer2 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
Sanjay, who forgets every 15 minutes, uses photos, notes, and tattoos to restore his memory
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.
Kotlin
// Memento class stores the state of the OriginatorclassMemento(val state: String)// Originator is the object whose state is savedclassOriginator(var state: String) {// Creates a Memento with the current statefuncreateMemento(): Memento {returnMemento(state) }// Restores the state from a Mementofunrestore(memento: Memento) {this.state = memento.state }funshowState() {println("Current State: $state") }}// Caretaker is responsible for storing and restoring the MementoclassCaretaker {privateval mementoList = mutableListOf<Memento>()// Adds a Memento to the listfunaddMemento(memento: Memento) { mementoList.add(memento) }// Retrieves the last saved Memento (undo functionality)fungetLastMemento(): Memento? {if (mementoList.isNotEmpty()) {return mementoList.removeAt(mementoList.size - 1) }returnnull }}// Demonstrating the Memento pattern in actionfunmain() {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 }}
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 or when 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.
The Strategy interface defines the contract for the algorithm.
OperationAdd, OperationSubtract, and OperationMultiply are concrete classes that implement the Strategy interface.
The Context class uses a Strategy to perform an operation.
In main(), we change the Strategy at runtime by passing different Strategy objects to the Context.
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
Kotlin
abstractclassGame {// Template methodfunplay() {initialize()startPlay()endPlay() }// Steps of the algorithm (template method components)abstractfuninitialize()abstractfunstartPlay()abstractfunendPlay()}
The play() method is the template method. It defines the skeleton of the algorithm.
The steps initialize(), startPlay(), and endPlay() are abstract and must be implemented by subclasses.
Create Concrete Classes
Chess Game
Kotlin
classChess : Game() {overridefuninitialize() {println("Chess Game Initialized. Set up the board.") }overridefunstartPlay() {println("Chess Game Started. Players are thinking about their moves.") }overridefunendPlay() {println("Chess Game Finished. Checkmate!") }}
Soccer Game
Kotlin
classSoccer : Game() {overridefuninitialize() {println("Soccer Game Initialized. Players are on the field.") }overridefunstartPlay() {println("Soccer Game Started. Kickoff!") }overridefunendPlay() {println("Soccer Game Finished. The final whistle blows.") }}
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 the play() method.
The play() method ensures the steps are executed in the defined order.
Concrete Classes:
The Chess and Soccer classes override the abstract methods to provide specific implementations for each step.
Reusability:
The play() method in the Game class ensures the overall structure of the algorithm is consistent across different games.
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.
Kotlin
abstractclassCookingRecipe {funcook() {prepareIngredients()cookMainPart()serve() }// Must be implemented by subclassesabstractfunprepareIngredients()// Default implementation, can be overriddenopenfuncookMainPart() {println("Cooking the main dish in a standard way") }// Hook methodopenfunserve() {println("Serving the dish") }}classPastaRecipe : CookingRecipe() {overridefunprepareIngredients() {println("Preparing pasta, sauce, and vegetables") }overridefuncookMainPart() {println("Cooking the pasta and sauce together") }// Override hook method to add custom behavioroverridefunserve() {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.
Defines specific behaviors for each type of element it visits.
May maintain state or data relevant to the operations performed during visitation.
Kotlin
classConcreteVisitor : Visitor {overridefunvisitConcreteElementA(element: ConcreteElementA) {// Specific operation for ConcreteElementA }overridefunvisitConcreteElementB(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 a Visitor and let it perform its operation.
Kotlin
interfaceVisitable {funaccept(visitor: Visitor)}
Concrete Visitable Classes
Implements the Visitable interface.
Provides the accept(visitor: Visitor) method implementation, which calls the appropriate visit() method on the visitor, passing itself as a parameter.
Create ConcreteVisitable classes: These are concrete shapes like Circle and Rectangle that implement the Shape interface and define the accept() method.
Client code: This is where we use the visitor pattern. We create the shapes and use the AreaCalculator to perform operations.
Kotlin
funmain() {val shapes: List<Shape> = listOf(Circle(5.0), Rectangle(4.0, 6.0))// Create the visitorval areaCalculator = AreaCalculator()// Visit each shape and calculate the area shapes.forEach { shape -> shape.accept(areaCalculator) }}//////////// OUTPUT //////////////////Calculating area of circle: ฯ * 5.0^2Area: 78.53981633974483Calculating area of rectangle: 4.0 * 6.0Area: 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.
When diving into the world of design patterns, one of the more intriguing patterns youโll encounter is the Visitor Design Pattern. While it might not be as commonly used as patterns like Singleton or Factory, it becomes incredibly powerful when you need to operate on a hierarchy of objects.
At first glance, the Visitor Design Pattern can seem challenging. Its name might feel familiar, yet the technical concept can appear tricky to grasp. But donโt worryโโโonce you understand its core principles, implementing it will be much easier than you expect.
In this guide, weโll break down the Visitor Pattern step by step. Weโll start by understanding its fundamentals, explore its structure in detail, and then finish with practical examples to solidify your understanding.
Ready to simplify this complex pattern? Letโs dive in and master the Visitor Design Pattern in Kotlin..!
What Is the 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.
Defines specific behaviors for each type of element it visits.
May maintain state or data relevant to the operations performed during visitation.
Kotlin
classConcreteVisitor : Visitor {overridefunvisitConcreteElementA(element: ConcreteElementA) {// Specific operation for ConcreteElementA }overridefunvisitConcreteElementB(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 a Visitor and let it perform its operation.
Kotlin
interfaceVisitable {funaccept(visitor: Visitor)}
Concrete Visitable Classes
Implements the Visitable interface.
Provides the accept(visitor: Visitor) method implementation, which calls the appropriate visit() method on the visitor, passing itself as a parameter.
Create ConcreteVisitable classes: These are concrete shapes like Circle and Rectangle that implement the Shape interface and define the accept() method.
Client code: This is where we use the visitor pattern. We create the shapes and use the AreaCalculator to perform operations.
Kotlin
funmain() {val shapes: List<Shape> = listOf(Circle(5.0), Rectangle(4.0, 6.0))// Create the visitorval areaCalculator = AreaCalculator()// Visit each shape and calculate the area shapes.forEach { shape -> shape.accept(areaCalculator) }}//////////// OUTPUT //////////////////Calculating area of circle: ฯ * 5.0^2Area: 78.53981633974483Calculating area of rectangle: 4.0 * 6.0Area: 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.
Real-world scenario: Campaigner visiting voters
This year has been a whirlwind of elections, including the US Presidential Election. On June 9, 2024, Narendra Modi was sworn in for his third term as Prime Minister of India. Election season is always full of energy, with promises aimed at addressing the needs of all sections of society.
Imagine a campaign in full swing: a candidate going door-to-door, tailoring their message for each voter. To high-income groups, they promise tax cuts to fuel growth. For low-income families, the focus is on welfare programs to improve daily life. For middle-income voters, the emphasis is on policies that ensure economic stability.
This adaptability mirrors the Visitor Design Pattern. It allows you to add new strategies or actionsโโโjust like a campaigner fine-tuning their pitchโโโwithout altering the core structure. Itโs flexible, efficient, and perfect for managing change.
Letโs implement the Visitor Design Pattern in Kotlin to see how it works.
Define the Visitor Interface
This represents the actions a Campaigner can take for different types of residents.
Each type of resident implements the accept() method to interact with the visitor.
Kotlin
classHighIncome : Resident {overridefunaccept(visitor: Visitor) { visitor.visit(this) // Calls the visitor's method for HighIncome }funincomeGroup() = "High Income Group"}classLowIncome : Resident {overridefunaccept(visitor: Visitor) { visitor.visit(this) // Calls the visitor's method for LowIncome }funincomeGroup() = "Low Income Group"}classMediumIncome : Resident {overridefunaccept(visitor: Visitor) { visitor.visit(this) // Calls the visitor's method for MediumIncome }funincomeGroup() = "Medium Income Group"}
Implement Concrete Visitor (Campaigner)
The campaigner performs different actions for each type of resident.
Kotlin
classCampaigner : Visitor {overridefunvisit(highIncome: HighIncome) {println("Campaigning to ${highIncome.incomeGroup()} with promises of tax cuts.") }overridefunvisit(lowIncome: LowIncome) {println("Campaigning to ${lowIncome.incomeGroup()} with promises of welfare programs.") }overridefunvisit(mediumIncome: MediumIncome) {println("Campaigning to ${mediumIncome.incomeGroup()} with promises of economic growth.") }}
Client Code (main())
The client creates residents and a campaigner, then executes the campaign.
Kotlin
funmain() {// List of residentsval residents: List<Resident> = listOf(HighIncome(),LowIncome(),MediumIncome() )// Create a campaigner (visitor)val campaigner = Campaigner()// Campaigner visits each resident residents.forEach { resident -> resident.accept(campaigner) // Accept allows double dispatch }}///////////////////// OUTPUT ///////////////////////////////Campaigning to High Income Group with promises of tax cuts.Campaigning to Low Income Group with promises of welfare programs.Campaigning to Medium Income Group with promises of economic growth.
First, letโs take a look at how Double Dispatch works here:
First Dispatch: The Resident objects (HighIncome, LowIncome, MediumIncome) call the accept() method and pass the Campaigner object to it.
Kotlin
resident.accept(campaigner) // First dispatch: HighIncome, LowIncome, MediumIncome
Second Dispatch: Inside the accept() method, the visitor (Campaigner) calls the appropriate visit() method based on the specific type of the Resident object.
Kotlin
campaigner.visit(this) // Second dispatch: visit() based on Resident type
This allows the Campaigner (visitor) to perform different actions depending on the concrete type of Resident (HighIncome, LowIncome, MediumIncome).
Other code is quite self-explanatory, so letโs take a look at the benefits we gain from using the Visitor Design Pattern.
Stable Resident Structure: You can add new behaviors, like a new type of campaigner, without changing the existing Resident hierarchy.
Flexible Behavior: To introduce a new visitor, like a Surveyor, you simply create a new classโโโno need to modify the Resident classes at all.
Encapsulation of Logic: Each visitor keeps its own behavior separate, meaning the Resident classes stay clean and focused on their core responsibilities.
This pattern works great when behaviors need to change often, but the overall structure of the objects remains stable.
Weโve talked a lot about the benefits, but the Visitor Pattern has its downsides too. Letโs take a look at them.
Inflexibility: Adding new element types requires changes to the visitor interface and all its implementations.
Coupling: Tight coupling between visitors and element classes.
Complexity: The pattern can increase complexity when there are numerous element types and visitors.
Conclusion
The Visitor pattern is a powerful way to add operations to object structures without modifying them. While it might seem complex initially, once you break it down (like we did here), its elegance becomes apparent. Itโs especially useful when working with stable hierarchies that require diverse operations.
By applying this pattern thoughtfully in Kotlin, we can write clean, maintainable, and extensible code.
I love cooking, but Iโm not a master chefโjust someone who enjoys experimenting in the kitchen. Wait, what am I talking about? How does this relate to the Template Method design pattern? Donโt worry; weโll uncover the mystery shortly. First, let me outline what weโll cover in this blog. Weโll start by understanding the Template Method Pattern, explore its structure, and then dive into some real-world use cases.
So, back to cooking! As I mentioned, I love it. I often watch recipes on YouTube and try to cook accordingly. Sometimes they turn out delicious; other times, not so much. But hereโs an important point I want to highlight: I never follow a recipe exactly. And I bet most of you donโt either. This is where the core idea of the Template Method design pattern comes into play.
When I watch a recipe, I use it as a guide but adapt it based on the ingredients I have or my personal preferences. Thereโs no strict rule that says I have to stick to the exact stepsโitโs up to me to modify it as I go. Similarly, the Template Method provides a skeleton or structure for an algorithm but allows flexibility to fill in the details and decide how to proceed.
I hope youโre starting to connect the dots. Donโt worryโit will all become clearer as we proceed further.
What is the 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
Kotlin
abstractclassGame {// Template methodfunplay() {initialize()startPlay()endPlay() }// Steps of the algorithm (template method components)abstractfuninitialize()abstractfunstartPlay()abstractfunendPlay()}
The play() method is the template method. It defines the skeleton of the algorithm.
The steps initialize(), startPlay(), and endPlay() are abstract and must be implemented by subclasses.
Create Concrete Classes
Chess Game
Kotlin
classChess : Game() {overridefuninitialize() {println("Chess Game Initialized. Set up the board.") }overridefunstartPlay() {println("Chess Game Started. Players are thinking about their moves.") }overridefunendPlay() {println("Chess Game Finished. Checkmate!") }}
Soccer Game
Kotlin
classSoccer : Game() {overridefuninitialize() {println("Soccer Game Initialized. Players are on the field.") }overridefunstartPlay() {println("Soccer Game Started. Kickoff!") }overridefunendPlay() {println("Soccer Game Finished. The final whistle blows.") }}
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 the play() method.
The play() method ensures the steps are executed in the defined order.
Concrete Classes:
The Chess and Soccer classes override the abstract methods to provide specific implementations for each step.
Reusability:
The play() method in the Game class ensures the overall structure of the algorithm is consistent across different games.
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.
Kotlin
abstractclassCookingRecipe {funcook() {prepareIngredients()cookMainPart()serve() }// Must be implemented by subclassesabstractfunprepareIngredients()// Default implementation, can be overriddenopenfuncookMainPart() {println("Cooking the main dish in a standard way") }// Hook methodopenfunserve() {println("Serving the dish") }}classPastaRecipe : CookingRecipe() {overridefunprepareIngredients() {println("Preparing pasta, sauce, and vegetables") }overridefuncookMainPart() {println("Cooking the pasta and sauce together") }// Override hook method to add custom behavioroverridefunserve() {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.
Real-World Use Case: Making a Beverage
To better understand the hook method, letโs consider another example:
Suppose weโre building a system to prepare beverages like tea and coffee. The preparation steps are generally similar:
Boil water.
Brew the beverage.
Pour it into a cup.
Add condiments.
However, the brewing and condiment steps vary between tea and coffee. This is where the Template Method Pattern and the Hook Method shine. Letโs implement it!
Define the Abstractย Class
Weโll create an abstract class Beverage that defines the template method prepareBeverage() and includes common steps.
Kotlin
abstractclassBeverage {// Template method: defines the skeleton of the algorithmfunprepareBeverage() {boilWater()brew()pourInCup()if (addCondimentsNeeded()) { // Hook to customize behavioraddCondiments() } }// Common steps with default implementationprivatefunboilWater() {println("Boiling water...") }privatefunpourInCup() {println("Pouring beverage into the cup...") }// Abstract steps to be implemented by subclassesabstractfunbrew()abstractfunaddCondiments()// Optional hook method for additional behavior customizationopenfunaddCondimentsNeeded(): Boolean = true}
Here,
prepareBeverage(): The template method, defining the algorithm in a step-by-step manner.
Common Methods: Steps like boiling water and pouring the beverage are common across all beverages, so we provide default implementations.
Abstract Methods: Methods like brew() and addCondiments() vary based on the beverage, so subclasses must define them.
classCoffee : Beverage() {overridefunbrew() {println("Brewing the coffee...") }overridefunaddCondiments() {println("Adding milk and sugar...") }overridefunaddCondimentsNeeded(): Boolean {// Assume the user doesn't want condiments for coffeereturnfalse }}
Here,
Tea: Implements brew() by steeping tea and adds lemon as a condiment.
Coffee: Implements brew() by brewing coffee and adds milk and sugar.
addCondimentsNeeded() in Coffee: Returns false to skip adding condiments. This demonstrates the use of the hook method.
Preparing tea:Boiling water...Steeping the tea...Pouring beverage into the cup...Adding lemon...Preparing coffee:Boiling water...Brewing the coffee...Pouring beverage into the cup...
Notice how the algorithm remains consistent, while the specific steps differ.
Advantages of the Template Methodย Pattern
Code Reusability: Common logic is centralized in the base class, reducing duplication.
Flexibility: Subclasses can customize specific steps without altering the algorithmโs structure.
Maintainability: Changes to the algorithm can be made in one place (the template method).
When to Use the Template Methodย Pattern?
When you have a set of similar processes with variations in specific steps.
When you want to enforce a specific sequence of steps.
When you need to provide optional hooks for behavior customization.
Conclusionย
The Template Method design pattern is like creating a blueprint for an algorithm. In Kotlin, it shines when you want to reuse code while still allowing flexibility for subclasses. By defining the โrecipeโ in a base class and letting subclasses fill in the details, we achieve a balance of consistency and customization.
I hope this explanation helped you understand the Template Method pattern in a relatable and clear way.
Happy Hooking with the Template Method Patternย ๐
Writing clean, maintainable, and scalable code is every developerโs goal, and design patterns play a big role in making that happen. One pattern that stands out for its flexibility and simplicity is the Strategy Design Pattern. It lets you define a group of algorithms, keep them separate, and easily switch between them when needed.ย
In this blog, weโll explore how the Strategy Design Pattern works in Kotlin, break down its implementation step by step, and look at some real-world examples to show how it can make your code more organized and easier to work with.
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 or when 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.
The Strategy interface defines the contract for the algorithm.
OperationAdd, OperationSubtract, and OperationMultiply are concrete classes that implement the Strategy interface.
The Context class uses a Strategy to perform an operation.
In main(), we change the Strategy at runtime by passing different Strategy objects to the Context.
Real-World Example: Payment System
I hope youโve grasped the basic concept and are now ready for a real-world code implementation. Letโs apply the Strategy Pattern to build a payment processing system where users can choose from different methods: Credit Card, PayPal, or Cryptocurrency.
Define the Strategy Interface
The PaymentStrategy interface will define a common method for all payment strategies.
Each class provides its unique implementation of the pay method. Notice how weโre encapsulating the logic specific to each payment method.
Create the Context
The PaymentContext class will use the selected PaymentStrategy to process payments.
Kotlin
// Context ClassclassPaymentContext(privatevar paymentStrategy: PaymentStrategy) {// Allows dynamic switching of strategyfunsetPaymentStrategy(strategy: PaymentStrategy) { paymentStrategy = strategy }// Delegates the payment to the selected strategyfunexecutePayment(amount: Double) { paymentStrategy.pay(amount) }}
The PaymentContext class acts as a bridge between the client code and the various payment strategies. It allows us to switch strategies on the fly using the setPaymentStrategy method.
Putting everything together
Now letโs see how we can use the Strategy Pattern.
Kotlin
funmain() {// Create specific payment strategiesval creditCardPayment = CreditCardPayment("1234-5678-9876-5432")val paypalPayment = PayPalPayment("[email protected]")val cryptoPayment = CryptoPayment("1A2b3C4d5E6F")// Create the context with an initial strategyval paymentContext = PaymentContext(creditCardPayment)// Execute payment using Credit Card paymentContext.executePayment(100.0)// Switch to PayPal strategy and execute payment paymentContext.setPaymentStrategy(paypalPayment) paymentContext.executePayment(200.0)// Switch to Cryptocurrency strategy and execute payment paymentContext.setPaymentStrategy(cryptoPayment) paymentContext.executePayment(300.0)}
Output
Kotlin
Paid $100.0 using Credit Card (Card Number: 1234-5678-9876-5432)Paid $200.0 using PayPal (Email: user@paypal.com)Paid $300.0 using Cryptocurrency (Wallet: 1A2b3C4d5E6F)
Basically, in India, we rarely use crypto for payments, but UPI payments are everywhere. Letโs add that here. The reason Iโm providing this context is to demonstrate how extensible this pattern is, in line with the Open/Closed Principle.
Kotlin
// New Concrete Strategy: UPI PaymentclassUPIPayment(privateval upiId: String) : PaymentStrategy {overridefunpay(amount: Double) {println("Paid $$amount using UPI (UPI ID: $upiId)") }}
One more thing Iโd like to highlight here is that the Strategy Pattern works well with Kotlinโs functional features. For simpler use cases, we can replace strategies with Kotlin lambdas to further reduce boilerplate code.
Here is a simple modification to test it.
Kotlin
classPaymentContextWithLambda {privatevar paymentStrategy: (Double) -> Unit = {}funsetPaymentStrategy(strategy: (Double) -> Unit) { paymentStrategy = strategy }funexecutePayment(amount: Double) {paymentStrategy(amount) }}funmain() {val paymentContext = PaymentContextWithLambda()// Using lambdas as strategies paymentContext.setPaymentStrategy { amount ->println("Paid $$amount using Credit Card") } paymentContext.executePayment(500.0) paymentContext.setPaymentStrategy { amount ->println("Paid $$amount using PayPal") } paymentContext.executePayment(600.0)}// Output// Paid $500.0 using Credit Card// Paid $600.0 using PayPal
If we closely look at everything here, weโll findโฆ
Encapsulation of Algorithms The PaymentStrategy interface encapsulates the algorithms (payment methods), ensuring each has a unique implementation in its respective class.
Dynamic Behavior The PaymentContext class allows you to switch payment methods dynamically by calling the setPaymentStrategy method.
Open/Closed Principle Adding a new payment method doesnโt require modifying existing classes. Instead, you just create a new implementation of PaymentStrategy.
Reusability The strategy classes (CreditCardPayment, PayPalPayment, etc.) can be reused in different contexts.
Benefits of Using the Strategyย Pattern
Eliminates Conditional Logic: No more lengthy if-else or when statements for selecting an algorithm.
Promotes Single Responsibility Principle: Each algorithm resides in its class, simplifying maintenance and testing.
Improves Flexibility: You can change strategies at runtime without impacting the client code.
Encourages Code Reuse: Strategies can be reused across different projects or modules.
Drawbacks of the Strategyย Pattern
Increased Number of Classes: Each algorithm requires its own class, which may clutter the codebase.
Context Dependency: The context relies on the strategy being set correctly before use, which might lead to runtime errors if not handled carefully.
When Not to Use the Strategyย Pattern
Avoid the Strategy Pattern if:
The algorithms are unlikely to change or expand.
Thereโs no need for runtime flexibility.
Conclusion
The Strategy Design Pattern is an elegant way to handle situations where multiple algorithms or behaviors are needed. Kotlinโs modern syntax makes implementing this pattern straightforward and flexible.
By encapsulating algorithms into individual classes or leveraging Kotlinโs lambda expressions, we can write code thatโs not only clean and modular but also adheres to key principles like the Open/Closed Principle.
So next time you find yourself writing a series of if or when statements to handle various behaviors, consider using the Strategy Pattern.ย
As developers, weโre all too familiar with hitting Ctrl + Z to undo our code changes. Itโs almost second nature at this point. But have you ever stopped to wonder what really happens when you hit that magic combination? Sure, we might think, โIt just stores the previous state somewhere and then brings it back,โ but thatโs only part of the story.
The real magic behind undoing your changes lies in the Memento Design Pattern. This pattern is what makes undo functionality so efficient and seamless. Curious to know how it works? Letโs dive in and take a closer look!
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
Sanjay, who forgets every 15 minutes, uses photos, notes, and tattoos to restore his memory
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.
Kotlin
// Memento class stores the state of the OriginatorclassMemento(val state: String)// Originator is the object whose state is savedclassOriginator(var state: String) {// Creates a Memento with the current statefuncreateMemento(): Memento {returnMemento(state) }// Restores the state from a Mementofunrestore(memento: Memento) {this.state = memento.state }funshowState() {println("Current State: $state") }}// Caretaker is responsible for storing and restoring the MementoclassCaretaker {privateval mementoList = mutableListOf<Memento>()// Adds a Memento to the listfunaddMemento(memento: Memento) { mementoList.add(memento) }// Retrieves the last saved Memento (undo functionality)fungetLastMemento(): Memento? {if (mementoList.isNotEmpty()) {return mementoList.removeAt(mementoList.size - 1) }returnnull }}// Demonstrating the Memento pattern in actionfunmain() {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 }}
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.
A few key points remain to be highlighted regarding how the Memento pattern works and its structure, particularly focusing on the concepts of a wide interface and a narrow interface.
What is a Wide Interface and a Narrow Interface?
In the context of the Memento Design Pattern, the terms wide interface and narrow interface typically refer to the scope of access provided by the Memento class.
Wide Interface: A Memento with a wide interface exposes more details of the internal state of the Originator. This allows the Caretaker to access and potentially modify the internal data of the Memento. This approach can lead to more flexibility but might break encapsulation.
Narrow Interface: A Memento with a narrow interface provides limited access to the internal state of the Originator. It hides the details of the state, ensuring that only essential information is made available. This helps maintain encapsulation and prevents external manipulation of the internal state, ensuring that the Originator remains in control.
In short,ย
Wide interface gives more access to the internal state.
Narrow interface restricts access to only necessary information, maintaining better encapsulation and control.
Real Scenario: A Text Editor with Undo Functionality
Letโs implement a simple text editor with undo functionality. Weโll use the Memento pattern to save and restore the editorโs state.
Define the Originator
The Originator is the class whose state we want to save and restore. In our case, itโs the TextEditor class.
Kotlin
// Originator: TextEditorclassTextEditor {var content: String = ""// Save current state as a Mementofunsave(): Memento {returnMemento(content) }// Restore state from a Mementofunrestore(memento: Memento) { content = memento.state }// Nested Memento class to encapsulate the statedataclassMemento(val state: String)}
Hereโs whatโs happening:
The content variable represents the editorโs current text.
The save() method creates a Memento containing the current state.
The restore() method updates the editorโs state using a Memento.
One more twist is that we used a Nested Memento, but it doesnโt really affect the approach. So, go ahead with this approach as well.
Create the Caretaker
The Caretaker manages the mementos. It decides when to save and restore the state. For simplicity, weโll use a stack (List) to store multiple mementos (for undo functionality).
Kotlin
// Caretaker: Manages mementosclassCaretaker {privateval mementoStack = mutableListOf<TextEditor.Memento>()// Save a mementofunsave(memento: TextEditor.Memento) { mementoStack.add(memento) }// Retrieve the last mementofunundo(): TextEditor.Memento? {if (mementoStack.isNotEmpty()) {return mementoStack.removeAt(mementoStack.size - 1) }returnnull }}
Here,
The save() method pushes a memento onto the stack.
The undo() method pops the last memento, providing the most recent state.
Tie Everything Together
Now letโs combine the Originator and Caretaker to see the Memento pattern in action.
Kotlin
funmain() {val textEditor = TextEditor()val caretaker = Caretaker()// Initial content textEditor.content = "Hello, World!"println("Content: ${textEditor.content}")// Save state caretaker.save(textEditor.save())// Modify content textEditor.content = "Hello, Kotlin!"println("Modified Content: ${textEditor.content}")// Save another state caretaker.save(textEditor.save())// Modify content again textEditor.content = "Design Patterns are fun!"println("Further Modified Content: ${textEditor.content}")// Undo last changeval lastState = caretaker.undo()if (lastState != null) { textEditor.restore(lastState)println("After Undo: ${textEditor.content}") }// Undo againval previousState = caretaker.undo()if (previousState != null) { textEditor.restore(previousState)println("After Another Undo: ${textEditor.content}") }}
Furthermore, we can apply this pattern to similar use cases, such as:
Games: Saving checkpoints or progress.
Graphical Editors: Reverting to earlier canvas states.
Benefits of the Memento Pattern
Encapsulation: The Memento class keeps the state private, adhering to encapsulation.
Undo/Redo: Itโs perfect for features like undo/redo in editors or games.
Simple and Scalable: Easy to implement and extend for more complex states.
Limitations of the Memento Pattern
Memory Usage: Storing many mementos can consume a lot of memory.
Complexity for Large States: If the state is large or includes references to other objects, managing mementos can get tricky.
Tips for Using the Memento Pattern in Kotlin
Immutable Mementos: Ensure mementos are immutable to avoid accidental changes.
Use Serialization for Complex States: For large or nested states, consider saving mementos using Kotlin serialization.
Limit History: To save memory, consider capping the number of mementos stored.
Conclusion
The Memento design pattern is a powerful tool for managing an objectโs state in a controlled and encapsulated way. In this blog, we implemented the pattern in Kotlin, showcasing its practical usage with a simple Text Editor example.
By mastering the Memento pattern, you can build applications that support undo/redo features or maintain state history without violating encapsulation principles. As seen in Kotlin, the pattern is intuitive and aligns well with the languageโs concise syntax and data class features.
Feel free to experiment with the pattern in your projects. Whether itโs a game, an editor, or a stateful application, the Memento pattern will prove invaluable!
Design patterns provide us with well-structured and reusable solutions to recurring problems. Today, weโll explore one such pattern that plays a crucial role in managing complex interactions between objectsโthe Mediator Design Pattern. In this blog, Iโll guide you through its concept, benefits, and how to implement it in Kotlin. By the end, youโll have a clear and solid understanding of the Mediator Design Pattern.
What is the 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.
Project Manager and Team Communication
If you remember, we discussed a real-world example earlierโProject Manager and Team. Now, letโs implement the Mediator design pattern for their communication.
Define the Mediator Interface
This interface allows the mediator to facilitate communication between colleagues.
Kotlin
// The Mediator interface defines how colleagues communicate via the mediatorinterfaceMediator {funsendMessage(message: String, colleague: Colleague)}
Create the Concrete Mediator
The ProjectManager acts as the mediator.
Kotlin
// Concrete Mediator (Project Manager) that implements the Mediator interfaceclassProjectManager : Mediator {privateval colleagues = mutableListOf<Colleague>()// Method to register colleagues (team members)funaddColleague(colleague: Colleague) { colleagues.add(colleague) }// The mediator routes the messages between colleaguesoverridefunsendMessage(message: String, colleague: Colleague) {// Forward the message to all other colleagues, but not to the sender colleagues.forEach {if (it != colleague) { it.receiveMessage(message, colleague.name) } } }}
Define the Colleague AbstractClass
This represents participants that communicate through the mediator.
Kotlin
// The Colleague class represents a team member who communicates through the mediatorabstractclassColleague(protectedval mediator: Mediator, val name: String) {// Send a message through the mediatorabstractfunsendMessage(message: String)// Receive a message from the mediatorabstractfunreceiveMessage(message: String, sender: String)}
Create Concrete Colleagues
The team members are the concrete colleagues.
Kotlin
// Concrete Colleague (Team Member) classes representing individual team membersclassTeamMember(mediator: Mediator, name: String) : Colleague(mediator, name) {// Implementing the sendMessage method, sends message through the mediatoroverridefunsendMessage(message: String) {println("$name sends message: \"$message\"") mediator.sendMessage(message, this) }// Implementing the receiveMessage method, where messages are received via the mediatoroverridefunreceiveMessage(message: String, sender: String) {println(" -> $name received message: \"$message\" from $sender") }}
Demonstrate the Pattern
Hereโs how it all comes together in the main function.
Kotlin
// Main function to run the simulationfunmain() {// Create a mediator (Project Manager)val projectManager = ProjectManager()// Create team members (colleagues)val akshay = TeamMember(projectManager, "Akshay") val ria = TeamMember(projectManager, "Ria") val amol = TeamMember(projectManager, "Amol") // Register team members with the mediator projectManager.addColleague(akshay) projectManager.addColleague(ria) projectManager.addColleague(amol)// Communication through the mediatorprintln("--- Communication Flow ---") akshay.sendMessage("Ria, have you completed the feature Alpha-Approval?") // Akshay sends a message to Ria ria.sendMessage("Yes, Akshay. I'll demo it soon.") // Ria replies to Akshay amol.sendMessage("Hey team, are there any blockers?") // Amol sends a message to the team akshay.sendMessage("No blockers from my side, Amol.") // Akshay responds to Amol ria.sendMessage("Same here, ready to deploy.") // Ria responds to Amol}
Output
Kotlin
--- Communication Flow ---Akshay sends message: "Ria, have you completed the feature Alpha-Approval?"-> Ria received message: "Ria, have you completed the feature Alpha-Approval?" from Akshay-> Amol received message: "Ria, have you completed the feature Alpha-Approval?" from AkshayRia sends message: "Yes, Akshay. I'll demo it soon."-> Akshay received message: "Yes, Akshay. I'll demo it soon." from Ria-> Amol received message: "Yes, Akshay. I'll demo it soon." from RiaAmol sends message: "Hey team, are there any blockers?"-> Akshay received message: "Hey team, are there any blockers?" from Amol-> Ria received message: "Hey team, are there any blockers?" from AmolAkshay sends message: "No blockers from my side, Amol."-> Ria received message: "No blockers from my side, Amol." from Akshay-> Amol received message: "No blockers from my side, Amol." from AkshayRia sends message: "Same here, ready to deploy."-> Akshay received message: "Same here, ready to deploy." from Ria-> Amol received message: "Same here, ready to deploy." from Ria
We implemented the Mediator design pattern using a Project Manager as the central point of coordination. In a typical team setup, communication and updates would flow directly through the Project Manager. But after the COVID-19 pandemic, team members started working remotely from different locations, and thatโs when communication shifted to platforms like chatrooms, WhatsApp groups, Slack, or other tools.
In this new setup, these platforms essentially became the mediators. They ensure that all communication flows through a central hub, which eliminates the need for team members to communicate directly with each other. This helps keep things organized and reduces confusion.
With this approach, using a generalized implementation of the Mediator design pattern (with the Project Manager as an example), messages are now clearly formatted, making it easier to track who sent the message and who received it. Symbols, like arrows (->), show the flow of messages from the sender to the recipients. This layout gives us a clear, visual representation of how the communication happens through the mediator.
Benefits of Using the Mediatorย Pattern
Reduced Complexity: By centralizing interactions, we eliminate the need for multiple direct references.
Improved Flexibility: Adding or modifying components becomes easier as they are only dependent on the mediator.
Enhanced Maintainability: The mediator encapsulates the interaction logic, making it easier to manage.
Potential Drawbacks
Single Point of Failure: The mediator can become a bottleneck or overly complex if not designed well.
Overhead: For simple scenarios, using a mediator may introduce unnecessary indirection.
Conclusion
The Mediator design pattern is a fantastic way to manage complex interactions in a decoupled, organized manner. Implementing it in Kotlin is both simple and powerful, allowing you to streamline communication by centralizing it through a mediator. This approach leads to cleaner, more maintainable, and easily extendable systems.
I hope this blog has helped clear up the Mediator pattern for you. If youโre working on a Kotlin project, give it a tryโitโs a great way to simplify your codebase.
Whatโs awesome about Kotlin is that the Mediator pattern fits seamlessly with advanced concepts like coroutines or dependency injection, giving you even more power and flexibility. Of course, itโs important to weigh the patternโs benefits against potential downsides, making sure it aligns with your projectโs needs.
Letโs keep learning and building amazing things together!
The Observer Design Pattern is a behavioral design pattern commonly used to build systems where multiple objects need to stay updated about changes in another object. This pattern promotes loose coupling and efficient communication between components, making it a staple in event-driven programming.
In this blog, weโll explore how the Observer pattern works, its use cases, and its implementation in Kotlin. Weโll break down the pattern step by step, provide code examples, and explain each part for clarity.
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.
However, there are a few other use cases, which we will now explore briefly, one by one.
Single SubjectโโโSingle Observer In this case, an observer can only observe one subject, and the subject is only watched by one observer. This setup is called a 1:1 association, where a notification about a change in the subjectโs state is sent to the corresponding observer.
Single SubjectโโโMultiple Observers This is the most common use of the Observer pattern, where a single subject is observed by multiple observers of different types. Whenever the subjectโs state changes, all the observers are notified. For example, if a central database changes its data, all the applications that depend on this database are notified.
Multiple SubjectsโโโSingle Observer In this case, a single observer watches several subjects at once. For example, a weather station might monitor different subjects like temperature, humidity, wind, etc. This is an m:1 association, where the observer watches multiple subjects.
Multiple SubjectsโโโMultiple Observers This scenario covers all the previous cases in an m:n association, where multiple observers watch multiple subjects. It happens when several observers want to observe many subjects at once.
In a weather monitoring system, there are multiple subjects such as a temperature sensor, humidity sensor, and rainfall sensor. These sensors provide real-time data about different weather conditions. Several observers are interested in this data: a weather app that displays updates to users, an agriculture system that monitors temperature and humidity to help with farming decisions, and a flood detection system that tracks rainfall to assess the risk of flooding. In this scenario, each observer monitors different subjects or a combination of them, receiving notifications whenever the sensors update their data. This is a typical example of Multiple SubjectsโโโMultiple Observers, where several subjects are being watched by different observers, each interested in specific data for different purposes.
Now that weโve covered enough of the theory, letโs move on to the structure and implementation of the observer pattern. From here, weโll focus on the most common and practical use case in programming: Single SubjectโโโMultiple 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
Kotlin
// Subject InterfaceinterfaceSubject {funattach(observer: Observer)fundetach(observer: Observer)funnotifyObservers()}// Observer InterfaceinterfaceObserver {funupdate()}// Concrete SubjectclassConcreteSubject : Subject {privateval observers = mutableListOf<Observer>()var state: String = ""set(value) {field = valuenotifyObservers() }// Attach an observeroverridefunattach(observer: Observer) { observers.add(observer)println("Observer added.") }// Detach an observeroverridefundetach(observer: Observer) { observers.remove(observer)println("Observer removed.") }// Notify all observers of a state changeoverridefunnotifyObservers() {println("Notifying observers...") observers.forEach { it.update() } }}// Concrete ObserverclassConcreteObserver(privateval id: String, privateval subject: ConcreteSubject) : Observer {privatevar observerState: String = ""// Update the observer's stateoverridefunupdate() { observerState = subject.stateprintln("Observer $id state updated to: $observerState") }}// Main function to demonstratefunmain() {// Create a concrete subjectval subject = ConcreteSubject()// Create observersval 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
Kotlin
Observer added.Observer added.Notifying observers...Observer 1 state updated to: State1Observer 2 state updated to: State1Notifying observers...Observer 1 state updated to: State2Observer 2 state updated to: State2Observer removed.Notifying observers...Observer 2 state updated to: State3
Here,
Both observers (observer1 and observer2) 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 only observer2 receives the update when the state changes to โState 3โ.
Real-World Example: Weatherย Station
Letโs implement a simple weather station where the WeatherStation acts as the Subject, and different displays (e.g., MobileDisplay, WebDisplay) act as Observers.
Define the Observer Interface
The Observer interface defines the contract for all Observers.
WeatherStation acts as the Subject and maintains a list of Observers (MobileDisplay and WebDisplay).
When setMeasurements is called, the Subject notifies all registered Observers about the state change.
Dynamic Subscription:
Observers like MobileDisplay and WebDisplay can dynamically register or unregister themselves from the Subject.
Loose Coupling:
The Subject and Observers interact only through their interfaces, ensuring loose coupling.
Real-Time Updates:
Observers are automatically updated whenever the Subjectโs state changes.
Hereโs another example to help clarify the observer pattern: our publicationโs newsletter system.
softAai Blogs Newsletter System
As we subscribe to YouTube channels to get the latest updates and videos, similarly, we have newsletters on Medium.com. This is a perfect example of the observer pattern, which is already in place. Letโs dissect it using our softAai Blogs newsletter and try to build a similar system with the observer pattern.
The idea is to notify subscribers of Medium publication, softAai Blogs, whenever I publish new articles. Our subscribersโโโwhether developers, learners, or tech enthusiastsโโโcan unsubscribe if they no longer want updates, or new readers can subscribe at any time.
Letโs design this system (hypothetically) using the Observer Pattern. Hereโs how it works:
softAai Blogs (Subject): Publishes new articles.
Subscribers (Observers): Get notified of the new articles.
Letโs translate this real-life scenario into code using Kotlin.
The Subscriber interface ensures all subscribers can handle updates (e.g., receiving a new articleโs title).
Implement the Concrete Subject
Kotlin
classsoftAaiNewsletter : Newsletter {privateval subscribers = mutableListOf<Subscriber>() // List of subscribersprivatevar latestArticle: String = ""overridefunaddSubscriber(subscriber: Subscriber) { subscribers.add(subscriber) }overridefunremoveSubscriber(subscriber: Subscriber) { subscribers.remove(subscriber) }overridefunnotifySubscribers() {for (subscriber in subscribers) { subscriber.receiveUpdate(latestArticle) } }// Publish a new articlefunpublishArticle(title: String) { latestArticle = titleprintln("New article published: $latestArticle")notifySubscribers() // Notify all subscribers }}
The softAaiNewsletter class maintains a list of subscribers and notifies them whenever a new article is published.
Implement the Concrete Observer
Kotlin
classBlogSubscriber(privateval name: String) : Subscriber {overridefunreceiveUpdate(articleTitle: String) {println("$name received the update: New article published - \"$articleTitle\"") }}
Each BlogSubscriber reacts to updates by printing the notification they receive.
Bringing It All Together
Hereโs how we connect everything.
Kotlin
funmain() {// Create the newsletterval softAaiNewsletter = softAaiNewsletter()// Create subscribersval subscriber1 = BlogSubscriber("amol")val subscriber2 = BlogSubscriber("akshay")val subscriber3 = BlogSubscriber("swapnil")// Add subscribers softAaiNewsletter.addSubscriber(subscriber1) softAaiNewsletter.addSubscriber(subscriber2) softAaiNewsletter.addSubscriber(subscriber3)// Publish an article softAaiNewsletter.publishArticle("Observer Pattern in Kotlin Explained")// Remove one subscriber softAaiNewsletter.removeSubscriber(subscriber2)// Publish another article softAaiNewsletter.publishArticle("Artificial Superintelligence (ASI): Unveiling the Genius")}
Output
Kotlin
New article published: ObserverPatterninKotlinExplainedamol received the update: Newarticlepublished - "Observer Pattern in Kotlin Explained"akshay received the update: Newarticlepublished - "Observer Pattern in Kotlin Explained"swapnil received the update: Newarticlepublished - "Observer Pattern in Kotlin Explained"New article published: ArtificialSuperintelligence (ASI): UnveilingtheGeniusamol received the update: Newarticlepublished - "Artificial Superintelligence (ASI): Unveiling the Genius"swapnil received the update: Newarticlepublished - "Artificial Superintelligence (ASI): Unveiling the Genius"
How It Relates to softAai Blogs
softAaiNewsletter (Subject): Represents our Medium newsletter system where new articles are published.
BlogSubscriber (Observer): Represents our readers who subscribe to the newsletter.
Publish Articles (Notify): Sends notifications to all subscribers about new articles.
Other Use Cases for the Observerย Pattern
The Observer pattern is widely used in various domains, including:
Graphical User Interfaces (GUIs): To update multiple components (e.g., text fields, labels) whenever the underlying data changes.
Event-driven Programming: For handling notifications such as click events, state changes, or messaging updates.
Event Systems: GUI libraries like Swing or JavaFX utilize the Observer pattern to manage event listeners effectively.
Data Binding: Frameworks like Androidโs LiveData or RxJava apply similar concepts to update UI components reactively.
Real-time Applications: To implement features like chat apps, stock market tickers, or dynamic news feeds.
Advantages of the Observerย Pattern
Loose Coupling: Subjects and observers are independent of each other, promoting modularity.
Reusability: Observers can be reused across different subjects.
Scalability: Easily add or remove observers without affecting the subject.
Limitations of the Observerย Pattern
Potential for Performance Issues: With many observers, frequent updates may impact performance.
Complexity: Managing dependencies between subjects and observers can become tricky in large systems.
Notification Overhead: Inefficient if only a subset of observers needs updates.
Conclusion
The Observer pattern is a cornerstone of effective software design, and Kotlinโs language features make it easy to implement. By using this pattern, we achieve a clean separation of concerns, enabling more modular and maintainable code.
I hope this guide has given you a solid understanding of the Observer pattern. Whether youโre building a notification system, implementing event-driven architectures, or working on real-time updates, this pattern will undoubtedly serve you well.
When it comes to design patterns, some are pretty straightforward, while others might seem a bit more complicated at first glance. One such pattern is the Interpreter design pattern. While it may sound like something youโd use only in very specific contexts, the Interpreter pattern can actually be quite handy when youโre dealing with languages or grammars, like when building parsers or evaluators. Today, Iโll walk you through the Interpreter pattern in a simple and approachable way, using Kotlin.
So, what exactly is the Interpreter design pattern? Letโs dive in!
What is the 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.
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.
Kotlin
// TerminalExpression class for numbersclassNumberExpression(privateval number: Int) : Expression {overridefuninterpret(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.
Kotlin
// NonTerminalExpression class for additionclassAddExpression(privateval left: Expression, privateval right: Expression) : Expression {overridefuninterpret(context: Map<String, Int>): Int {return left.interpret(context) + right.interpret(context) }}// NonTerminalExpression class for subtractionclassSubtractExpression(privateval left: Expression, privateval right: Expression) : Expression {overridefuninterpret(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.
Kotlin
funmain() {// Build the expression treeval 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 resultprintln("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) and NumberExpression(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:
Kotlin
// Context: x = 3, y = 5val context = mapOf("x" to 3, "y" to 5)
Advantages of Using the Interpreter Pattern
Extensibility: Itโs easy to add more expressions or operators without affecting the existing code. For example, if we wanted to add multiplication or division, we could simply create new NonTerminalExpression classes for those operations.
Maintainability: The expression logic is separated into individual components, making the code cleaner and easier to maintain.
Readability: With the use of well-named classes (like AddExpression, NumberExpression), the code becomes more understandable and easier to extend.
Disadvantages
Complexity: For simple scenarios, the Interpreter pattern might introduce unnecessary complexity. If the problem doesnโt require a structured approach, a simpler solution might be more appropriate.
Performance: In cases with large and complex expression trees, the recursive nature of the Interpreter pattern could lead to performance issues. It might not be the best choice for very large grammars.
Conclusion
The Interpreter design pattern is like building a Lego set for a specific problemโit lets you piece together small, reusable blocks (expressions) into a complete solution. While it might not be the go-to pattern for every scenario, itโs a powerful tool when you need to evaluate structured rules or languages.
In Kotlin, this pattern feels natural thanks to its object-oriented features and functional programming capabilities. So next time youโre dealing with something like custom DSLs, math interpreters, or even mini-scripting engines, give the Interpreter pattern a try!