Design Patterns

Observer Design Pattern

Understanding the Observer Design Pattern in Kotlin: A Comprehensive Guide

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:

  1. Subject: The object that holds the state and notifies observers of changes.
  2. Observer: The object that wants to be notified about changes in the subject.
  3. Concrete Subject: A specific implementation of the subject.
  4. Concrete Observer: A specific implementation of the observer.

Implementation

Kotlin
// Subject Interface
interface Subject {
    fun attach(observer: Observer)
    fun detach(observer: Observer)
    fun notifyObservers()
}

// Observer Interface
interface Observer {
    fun update()
}

// Concrete Subject
class ConcreteSubject : Subject {
    private val observers = mutableListOf<Observer>()
    var state: String = ""
        set(value) {
            field = value
            notifyObservers()
        }

    // Attach an observer
    override fun attach(observer: Observer) {
        observers.add(observer)
        println("Observer added.")
    }

    // Detach an observer
    override fun detach(observer: Observer) {
        observers.remove(observer)
        println("Observer removed.")
    }

    // Notify all observers of a state change
    override fun notifyObservers() {
        println("Notifying observers...")
        observers.forEach { it.update() }
    }
}

// Concrete Observer
class ConcreteObserver(private val id: String, private val subject: ConcreteSubject) : Observer {
    private var observerState: String = ""

    // Update the observer's state
    override fun update() {
        observerState = subject.state
        println("Observer $id state updated to: $observerState")
    }
}

// Main function to demonstrate
fun main() {
    // Create a concrete subject
    val subject = ConcreteSubject()

    // Create observers
    val observer1 = ConcreteObserver("1", subject)
    val observer2 = ConcreteObserver("2", subject)

    // Attach observers to the subject
    subject.attach(observer1)
    subject.attach(observer2)

    // Change the subject's state and notify observers
    subject.state = "State 1"
    subject.state = "State 2"

    // Detach an observer and change the state
    subject.detach(observer1)
    subject.state = "State 3"
}

Output

Kotlin
Observer added.
Observer added.
Notifying observers...
Observer 1 state updated to: State 1
Observer 2 state updated to: State 1
Notifying observers...
Observer 1 state updated to: State 2
Observer 2 state updated to: State 2
Observer removed.
Notifying observers...
Observer 2 state updated to: State 3

Here,

  1. Both observers (observer1 and observer2) are attached to the subject.
  2. When the state changes to “State 1”, both observers receive the update.
  3. When the state changes to “State 2”, both observers again receive the update.
  4. 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.

Kotlin
interface Observer {
    fun update(temperature: Float, humidity: Float, pressure: Float)
}

Define the Subject Interface

The Subject interface declares methods for managing Observers.

Kotlin
interface Subject {
    fun addObserver(observer: Observer)
    fun removeObserver(observer: Observer)
    fun notifyObservers()
}

Implement the WeatherStation (ConcreteSubject)

The WeatherStation keeps track of weather data and notifies Observers whenever data changes.

Kotlin
class WeatherStation : Subject {
    private val observers = mutableListOf<Observer>()
    private var temperature: Float = 0.0f
    private var humidity: Float = 0.0f
    private var pressure: Float = 0.0f

    override fun addObserver(observer: Observer) {
        observers.add(observer)
    }

    override fun removeObserver(observer: Observer) {
        observers.remove(observer)
    }

    override fun notifyObservers() {
        for (observer in observers) {
            observer.update(temperature, humidity, pressure)
        }
    }

    fun setMeasurements(temperature: Float, humidity: Float, pressure: Float) {
        this.temperature = temperature
        this.humidity = humidity
        this.pressure = pressure
        notifyObservers()
    }
}

Implement Concrete Observers

Each Observer implements the Observer interface and reacts to changes.

Mobile Display

Kotlin
class MobileDisplay(private val weatherStation: Subject) : Observer {
    init {
        weatherStation.addObserver(this)
    }

    override fun update(temperature: Float, humidity: Float, pressure: Float) {
        println("Mobile Display - Temperature: $temperature, Humidity: $humidity, Pressure: $pressure")
    }
}

Web Display

Kotlin
class WebDisplay(private val weatherStation: Subject) : Observer {
    init {
        weatherStation.addObserver(this)
    }

    override fun update(temperature: Float, humidity: Float, pressure: Float) {
        println("Web Display - Temperature: $temperature, Humidity: $humidity, Pressure: $pressure")
    }
}

Let’s Test the Implementation

Kotlin
fun main() {
    val weatherStation = WeatherStation()

    val mobileDisplay = MobileDisplay(weatherStation)
    val webDisplay = WebDisplay(weatherStation)

    // Simulate weather updates
    weatherStation.setMeasurements(25.0f, 65.0f, 1013.0f)
    weatherStation.setMeasurements(28.0f, 70.0f, 1012.0f)

    // Remove a display
    weatherStation.removeObserver(mobileDisplay)
    weatherStation.setMeasurements(30.0f, 75.0f, 1011.0f)
}

Output

Kotlin
Mobile Display - Temperature: 25.0, Humidity: 65.0, Pressure: 1013.0
Web Display - Temperature: 25.0, Humidity: 65.0, Pressure: 1013.0
Mobile Display - Temperature: 28.0, Humidity: 70.0, Pressure: 1012.0
Web Display - Temperature: 28.0, Humidity: 70.0, Pressure: 1012.0
Web Display - Temperature: 30.0, Humidity: 75.0, Pressure: 1011.0

Here,

Subject-Observer Relationship:

  • 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.

Create the Subject Interface

Kotlin
interface Newsletter {
    fun addSubscriber(subscriber: Subscriber)
    fun removeSubscriber(subscriber: Subscriber)
    fun notifySubscribers()
}

Here, the Newsletter interface defines methods to manage subscribers and send updates.

Create the Observer Interface

Kotlin
interface Subscriber {
    fun receiveUpdate(articleTitle: String)
}

The Subscriber interface ensures all subscribers can handle updates (e.g., receiving a new article’s title).

Implement the Concrete Subject

Kotlin
class softAaiNewsletter : Newsletter {
    private val subscribers = mutableListOf<Subscriber>() // List of subscribers
    private var latestArticle: String = ""

    override fun addSubscriber(subscriber: Subscriber) {
        subscribers.add(subscriber)
    }

    override fun removeSubscriber(subscriber: Subscriber) {
        subscribers.remove(subscriber)
    }

    override fun notifySubscribers() {
        for (subscriber in subscribers) {
            subscriber.receiveUpdate(latestArticle)
        }
    }

    // Publish a new article
    fun publishArticle(title: String) {
        latestArticle = title
        println("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
class BlogSubscriber(private val name: String) : Subscriber {
    override fun receiveUpdate(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
fun main() {
    // Create the newsletter
    val softAaiNewsletter = softAaiNewsletter()

    // Create subscribers
    val 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: Observer Pattern in Kotlin Explained
amol received the update: New article published - "Observer Pattern in Kotlin Explained"
akshay received the update: New article published - "Observer Pattern in Kotlin Explained"
swapnil received the update: New article published - "Observer Pattern in Kotlin Explained"
New article published: Artificial Superintelligence (ASI): Unveiling the Genius
amol received the update: New article published - "Artificial Superintelligence (ASI): Unveiling the Genius"
swapnil received the update: New article published - "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

  1. Loose Coupling: Subjects and observers are independent of each other, promoting modularity.
  2. Reusability: Observers can be reused across different subjects.
  3. Scalability: Easily add or remove observers without affecting the subject.

Limitations of the Observer Pattern

  1. Potential for Performance Issues: With many observers, frequent updates may impact performance.
  2. Complexity: Managing dependencies between subjects and observers can become tricky in large systems.
  3. 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.

Interpreter Design Pattern

Understanding the Interpreter Design Pattern in Kotlin: A Comprehensive Guide

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:

  1. AbstractExpression: This defines the interface for all expressions. It usually has an interpret() method, which is responsible for interpreting the expression.
  2. 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.
  3. 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.
  4. Context: This holds the data or the input we want to interpret.

When Should We Use It?

The Interpreter pattern comes in handy when:

  1. We need to evaluate a series of expressions that follow some grammar or rules.
  2. We’re dealing with complex expressions that can be broken down into smaller components.
  3. 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.

Kotlin
// AbstractExpression interface
interface Expression {
    fun interpret(context: Map<String, Int>): Int
}

Here, the interpret method takes a context (which can be a map of variable values) and returns an integer result.

Create Terminal Expressions

Now, let’s create terminal expressions. These are the base expressions, like numbers in the expression.

Kotlin
// TerminalExpression class for numbers
class NumberExpression(private val number: Int) : Expression {
    override fun interpret(context: Map<String, Int>): Int {
        return number
    }
}

In this class, we simply store a number, and when we interpret it, we return that number.

Create Non-Terminal Expressions

Next, we’ll implement the non-terminal expressions. These are the operators like addition or subtraction. Each non-terminal expression will hold references to two sub-expressions: the left-hand side and the right-hand side.

Kotlin
// NonTerminalExpression class for addition
class AddExpression(private val left: Expression, private val right: Expression) : Expression {
    override fun interpret(context: Map<String, Int>): Int {
        return left.interpret(context) + right.interpret(context)
    }
}

// NonTerminalExpression class for subtraction
class SubtractExpression(private val left: Expression, private val right: Expression) : Expression {
    override fun interpret(context: Map<String, Int>): Int {
        return left.interpret(context) - right.interpret(context)
    }
}

Here, the AddExpression and SubtractExpression are the operators, and they each hold two Expression objects, representing the left and right operands. When we interpret them, we recursively interpret both sides and then apply the operation. Basically each of these expressions takes two sub-expressions (left and right) and performs an operation on their results.

Build the Expression Tree (Bringing All Together)

Now that we’ve created our expressions, we can evaluate them as a tree, where each node represents an operation and the leaves are the numbers. Let’s explore how these components come together in a simple interpreter.

Kotlin
fun main() {
    // Build the expression tree
    val expression = AddExpression(
        NumberExpression(3),
        SubtractExpression(NumberExpression(5), NumberExpression(2))
    )

    // Create a context if needed (in this case, we don't need it, so we use an empty map)
    val result = expression.interpret(emptyMap())

    // Print the result
    println("Result: $result")  // Output will be 3 + (5 - 2) = 6
}

Here,

Expression Tree Construction: To begin, we construct an expression tree. At the root, we have an AddExpression, which consists of two child nodes:

  • The left child is a NumberExpression(3).
  • The right child is a SubtractExpression, which further has two children: NumberExpression(5) 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 = 5
  val 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!

Until next time, happy coding! 😊

Iterator Design Pattern

The Iterator Design Pattern in Kotlin: Simplified and Explained

When working with collections or data structures in Kotlin (or any programming language), iterating through elements is a common task. But what if you need greater control over how you traverse a collection? This is where the Iterator Design Pattern comes into play. In this article, we’ll delve into the concept of the Iterator Design Pattern, its practical implementation in Kotlin, and break it down step by step for better understanding.

Iterator Design Pattern

To iterate simply means to repeat an action. In software, iteration can be achieved using either recursion or loop structures, like for and while loops. When we need to provide functionality for iteration in a class, we often use something called an iterator.

Now, let’s talk about aggregates. Think of an aggregate as a collection of objects. It could be implemented in various forms, such as an array, a vector, or even a binary tree — essentially, any structure that holds multiple objects.

The iterator design pattern offers a structured way to handle how aggregates and their iterators are implemented. This pattern is based on two key design principles:

Separation of Concerns
This principle encourages us to keep different functionalities in separate areas. In the context of iterators, it means splitting the responsibility:

  • The aggregate focuses solely on managing (Means storing and organizing) its collection of objects.
  • The iterator takes care of traversing through the aggregate.

By doing this, we ensure that the code for maintaining the collection is cleanly separated from the code that deals with traversing it.

Decoupling of Data and Operations
This principle, rooted in generic programming, emphasizes independence between data structures and the operations performed on them. In short, the iterator pattern allows us to create traversal logic that works independently of the underlying data structure — whether it’s an array, a tree, or something else. This makes the traversal code more reusable and adaptable.

In practice, this design pattern simplifies things by moving the traversal logic out of the aggregate and into a dedicated iterator. This way, the aggregate focuses on its core responsibility — managing data — while the iterator focuses on efficiently navigating through that data. By adhering to these principles, we get cleaner, more modular, and reusable code.

Structure of the Iterator Design Pattern

Basically, here:

  • Iterator: Defines an interface for accessing and traversing elements.
  • Concrete Iterator: Implements the Iterator interface and provides the mechanism for iteration.
  • Aggregate: Represents the collection of elements.
  • Concrete Aggregate: Implements the collection (Aggregate) interface and returns an iterator to traverse its elements.

Now, let’s implement the Iterator Pattern in Kotlin

Iterator Interface

Kotlin
interface Iterator<T> {
    fun first(): T
    fun next(): T
    fun isDone(): Boolean
    fun currentItem(): T
}

Defines the standard methods First(), Next(), IsDone(), and CurrentItem().

ConcreteIterator

Implements these methods and provides specific logic for iterating over a list of items.

Kotlin
class ConcreteIterator<T>(private val items: List<T>) : Iterator<T> {
    private var currentIndex = 0

    override fun first(): T {
        return items[0]  // Return the first item
    }

    override fun next(): T {
        if (!isDone()) {
            return items[currentIndex++]  // Move to next and return the current item
        }
        throw NoSuchElementException("No more items.")
    }

    override fun isDone(): Boolean {
        return currentIndex >= items.size  // Check if we've iterated past the last item
    }

    override fun currentItem(): T {
        if (isDone()) throw NoSuchElementException("No more items.")
        return items[currentIndex]  // Return the current item
    }
}

Here, 

  • first(): Returns the first item in the list.
  • next(): Returns the next item and increments the index.
  • isDone(): Checks if all items have been traversed.
  • currentItem(): Returns the current item.

Aggregate Interface

Kotlin
interface Aggregate<T> {
    fun createIterator(): Iterator<T>
}

The Aggregate interface only defines the createIterator() method that will return an iterator.

ConcreteAggregate

Kotlin
class ConcreteAggregate<T>(private val items: List<T>) : Aggregate<T> {
    override fun createIterator(): Iterator<T> {
        return ConcreteIterator(items)  // Return a new ConcreteIterator
    }
}

The ConcreteAggregate class implements Aggregate, and its createIterator() method returns a new instance of ConcreteIterator to iterate over the collection.

Client Code

The client creates an aggregate and uses the iterator to traverse the items in the collection.

Kotlin
fun main() {
    val books = listOf("Let Us C", "Mastering Kotlin", "Wings of Fire", "Life Lessons")
    
    val bookCollection = ConcreteAggregate(books)
    val iterator = bookCollection.createIterator()

    println("First item: ${iterator.first()}")
    
    while (!iterator.isDone()) {
        println("Current item: ${iterator.currentItem()}")
        iterator.next()
    }
}

Output

Kotlin
First item: Let Us C
Current item: Let Us C
Current item: Mastering Kotlin
Current item: Wings of Fire
Current item: Life Lessons

Real-World Use Case

Let’s implement a real-world example of iterating through a collection of books in a library. 📚 It’s just an extension with a few modifications, but it’s more relatable. So, stay with me until the iteration ends. 😊

Define the Iterator Interface

The Iterator interface defines the contract for iterating through a collection.

Kotlin
interface Iterator<T> {
    fun hasNext(): Boolean // Checks if there's a next element
    fun next(): T          // Returns the next element
}

Create the Aggregate Interface

The Aggregate interface represents a collection that can return an iterator.

Kotlin
interface Aggregate<T> {
    fun createIterator(): Iterator<T>
}

Create the Concrete Aggregate

Now, let’s define a Library class that holds a collection of books.

Kotlin
data class Book(val title: String, val author: String)

class Library(private val books: List<Book>) : Aggregate<Book> {
    override fun createIterator(): Iterator<Book> = BookIterator(books)
}

Implement the Concrete Iterator

The BookIterator will traverse the Library.

Kotlin
class BookIterator(private val books: List<Book>) : Iterator<Book> {
    private var index = 0 // Keeps track of the current position
    
    override fun hasNext(): Boolean {
        // Returns true if there are more books to iterate over
        return index < books.size
    }
    
    override fun next(): Book {
        // Returns the current book and moves to the next one
        if (!hasNext()) throw NoSuchElementException("No more books in the library!")
        return books[index++]
    }
}

Bringing It All Together

Let’s use the Library and BookIterator to see the pattern in action.

Kotlin
fun main() {
    // Creating a list of books
    val books = listOf(
        Book("Let Us C", "Yashavant Kanetkar"),
        Book("Mastering Kotlin", "Naveen Tamrakar"),
        Book("Wings of Fire", "A.P.J. Abdul Kalam"),
        Book("Life Lessons", "Gaur Gopal Das")
    )

    // Creating a Library
    val library = Library(books)

    // Getting an iterator for the library
    val iterator = library.createIterator()

    // Traversing the library
    println("Books in the Library:")
    while (iterator.hasNext()) {
        val book = iterator.next()
        println("${book.title} by ${book.author}")
    }
}

Output

Kotlin
Books in the Library:
Let Us C by Yashavant Kanetkar
Mastering Kotlin by Naveen Tamrakar
Wings of Fire by A.P.J. Abdul Kalam
Life Lessons by Gaur Gopal Das

Adding a Reverse Iterator

Let’s add a ReverseBookIterator to iterate through the books in reverse order. While we could use method names like hasPrevious() or prev(), we opted to avoid them to maintain simplicity and consistency in the code.

Kotlin
class ReverseBookIterator(private val books: List<Book>) : Iterator<Book> {
    private var index = books.size - 1 // Start from the last book

    override fun hasNext(): Boolean {
        return index >= 0
    }

    override fun next(): Book {
        if (!hasNext()) throw NoSuchElementException("No more books in reverse order!")
        return books[index--]
    }
}

Modify the Library class to provide this reverse iterator.

Kotlin
fun createReverseIterator(): Iterator<Book> = ReverseBookIterator(books)

Now you can iterate in reverse.

Kotlin
val reverseIterator = library.createReverseIterator()
println("\nBooks in Reverse Order:")
while (reverseIterator.hasNext()) {
    val book = reverseIterator.next()
    println("${book.title} by ${book.author}")
}

You might be asking, “Why not just use a regular for loop or Kotlin’s built-in iterators?” Well, that’s a great question! Let me explain why the Iterator pattern could be a better fit:

  1. Custom Traversal Logic: With the Iterator pattern, you can easily implement custom traversal logic, like iterating in reverse order. This gives you more control compared to a basic for loop.
  2. Abstraction: By using an iterator, you hide the internal structure of your collection. This means the client code doesn’t need to worry about how the data is stored or how it’s being accessed.
  3. Flexibility: The Iterator pattern allows you to swap out different iterators without modifying the client code. This makes your solution more adaptable to changes in the future.

So, while a simple for loop might seem like a quick solution, using the Iterator pattern provides more flexibility, control, and abstraction in your code.

Kotlin’s Built-in Iterators

In real-world scenarios, you might not always need to implement your own iterators. Kotlin provides robust support for iterators out of the box through collections like List, Set, and Map.

Kotlin
val books = listOf(
    Book("Let Us C", "Yashavant Kanetkar"),
        Book("Mastering Kotlin", "Naveen Tamrakar"),
        Book("Wings of Fire", "A.P.J. Abdul Kalam"),
        Book("Life Lessons", "Gaur Gopal Das")
)

for (book in books) {
    println("${book.title} by ${book.author}")
}

Kotlin’s built-in iterators are efficient and follow the same principles as the Iterator pattern.

Best Practices for Using the Iterator Pattern in Kotlin

  • Leverage Kotlin’s Built-In Iterators: Kotlin’s collections (List, Set, Map) come with built-in iterators like forEach, iterator(), and more. Use the pattern when custom traversal logic is required.
  • Favor Readability: Ensure your implementation is easy to understand, especially when designing iterators for complex collections.

Advantages of the Iterator Pattern

  1. Decouples Collection and Traversal: With the Iterator pattern, the collection doesn’t need to know how its elements are being traversed. This separation of concerns makes the code cleaner and more maintainable.
  2. Uniform Traversal Interface: No matter what kind of collection you’re working with, the traversal process remains consistent. This gives you a unified way to access different types of collections without worrying about their internal structures.
  3. Supports Multiple Iterators: The Iterator pattern allows you to have multiple iterators working with the same collection at the same time. This means you can have different ways of iterating over the collection without them interfering with each other.

By using the Iterator pattern, you gain more flexibility, clarity, and control when working with collections..!

Conclusion

The Iterator Design Pattern isn’t about reinventing the wheel; it’s about designing systems that are flexible, reusable, and maintainable. In Kotlin, where we already have robust collections and iterator support, this pattern might seem overkill for basic use cases. But when you need custom traversal logic or want to decouple traversal from collection, this pattern becomes a game-changer.

I hope this explanation gave you a clear picture of how the Iterator pattern works.

Happy Iterating…~~~…~~~…!

Command Design Pattern

Don’t Let the Command Design Pattern Confuse You in Kotlin: A Simple Guide with Practical Insights

The Command Design Pattern is a behavioral design pattern that encapsulates a request as an independent object, storing all necessary information to process the request. This pattern is especially beneficial in scenarios where you need to:

  • Separate the initiator of the request (the sender) from the object responsible for executing it (the receiver).
  • Implement functionality for undoing and redoing operations.
  • Facilitate the queuing, logging, or scheduling of requests for execution.

Let’s embark on a journey to understand the Command design pattern, its purpose, and how we can implement it in Kotlin. Along the way, I’ll share practical examples and insights to solidify our understanding.

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:

  1. Command: An interface or abstract class defining a single method, execute().
  2. ConcreteCommand: Implements the Command interface and encapsulates the actions to be performed.
  3. Receiver: The object that performs the actual work.
  4. Invoker: The object that triggers the command’s execution.
  5. 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.kt
interface Command {
    fun execute()
}

Create Receivers

The receiver performs the actual actions. For simplicity, we’ll create two receivers: Light and MusicPlayer.

Kotlin
// Light.kt
class Light {
    fun turnOn() {
        println("Light is turned ON")
    }

    fun turnOff() {
        println("Light is turned OFF")
    }
}

// MusicPlayer.kt
class MusicPlayer {
    fun playMusic() {
        println("Music is now playing")
    }

    fun stopMusic() {
        println("Music is stopped")
    }
}

Create Concrete Commands

Each concrete command encapsulates a request to the receiver.

Kotlin
// LightCommands.kt
class TurnOnLightCommand(private val light: Light) : Command {
    override fun execute() {
        light.turnOn()
    }
}

class TurnOffLightCommand(private val light: Light) : Command {
    override fun execute() {
        light.turnOff()
    }
}

// MusicCommands.kt
class PlayMusicCommand(private val musicPlayer: MusicPlayer) : Command {
    override fun execute() {
        musicPlayer.playMusic()
    }
}

class StopMusicCommand(private val musicPlayer: MusicPlayer) : Command {
    override fun execute() {
        musicPlayer.stopMusic()
    }
}

Create the Invoker

The invoker doesn’t know the details of the commands but can execute them. In this case, our remote is the center of home automation and can control everything.

Kotlin
// RemoteControl.kt
class RemoteControl {
    private val commands = mutableListOf<Command>()

    fun setCommand(command: Command) {
        commands.add(command)
    }

    fun executeCommands() {
        commands.forEach { it.execute() }
        commands.clear()
    }
}

Client Code

Now, let’s create the client code to see the pattern in action.

Kotlin
// Main.kt
fun main() {
    // Receivers
    val light = Light()
    val musicPlayer = MusicPlayer()

    // Commands
    val turnOnLight = TurnOnLightCommand(light)
    val turnOffLight = TurnOffLightCommand(light)
    val playMusic = PlayMusicCommand(musicPlayer)
    val stopMusic = StopMusicCommand(musicPlayer)

    // Invoker
    val remoteControl = RemoteControl()

    // Set and execute commands
    remoteControl.setCommand(turnOnLight)
    remoteControl.setCommand(playMusic)
    remoteControl.executeCommands() // Executes: Light ON, Music Playing

    remoteControl.setCommand(turnOffLight)
    remoteControl.setCommand(stopMusic)
    remoteControl.executeCommands() // Executes: Light OFF, Music Stopped
}

Here,

Command Interface: The Command interface ensures uniformity. Whether it’s turning on a light or playing music, all commands implement execute().

Receivers: The Light and MusicPlayer classes perform the actual work. They are decoupled from the invoker.

Concrete Commands: Each command bridges the invoker and the receiver. This encapsulation allows us to add new commands easily without modifying the existing code (We will see it shortly after this).

Invoker: The RemoteControl acts as a controller. It queues and executes commands, providing flexibility for batch operations.

Client Code: We bring all components together, creating a functional smart home system.

Enhancing the Pattern

If we wanted to add undo functionality, we could introduce an undo() method in the Command interface. Each concrete command would then implement the reversal logic. For example:

Kotlin
interface Command {
    fun execute()
    fun undo()
}

class TurnOnLightCommand(private val light: Light) : Command {
    override fun execute() {
        light.turnOn()
    }

    override fun undo() {
        light.turnOff()
    }
}

Advantages of the Command Pattern

Flexibility: Adding new commands doesn’t affect existing code, adhering to the Open/Closed Principle.

Decoupling: The sender (invoker) doesn’t need to know about the receiver’s implementation.

Undo/Redo Support: With a little modification, you can store executed commands and reverse their actions.

Command Queues: Commands can be queued for delayed execution.

Disadvantages

Complexity: For simple use cases, this pattern may feel overengineered due to the additional classes.

Memory Overhead: Keeping a history of commands may increase memory usage.

Conclusion

The Command design pattern is a powerful tool in a developer’s toolkit, allowing us to build systems that are flexible, decoupled, and easy to extend. Kotlin’s concise and expressive syntax makes implementing this pattern a breeze, ensuring both clarity and functionality.

I hope this deep dive has demystified the Command pattern for you. Now it’s your turn — experiment with the code, tweak it, and see how you can apply it in your own projects..! 

Happy coding..! 🚀

State Design Pattern

Gain Clarity on the State Design Pattern in Kotlin: A Step-by-Step Guide

Have you ever had to manage an object’s behavior based on its state? You might have ended up writing a series of if-else or when statements to handle different scenarios. Sound familiar? (Especially if you’re working with Android and Kotlin!) If so, it’s time to explore the State Design Pattern—a structured approach to simplify your code, enhance modularity, and improve maintainability.

In this blog, we’ll explore the State Design Pattern in depth, focusing on its use in Kotlin. We’ll discuss its purpose, the benefits it offers, and provide detailed examples with clear explanations. By the end, you’ll have the knowledge and confidence to incorporate it seamlessly into your projects.

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
interface State {
    fun handle(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
class ConcreteStateA : State {
    override fun handle(context: Context) {
        println("State A: Handling request and transitioning to State B")
        context.setState(ConcreteStateB()) // Transition to State B
    }
}

class ConcreteStateB : State {
    override fun handle(context: Context) {
        println("State B: Handling request and transitioning to State A")
        context.setState(ConcreteStateA()) // Transition to State A
    }
}
  • ConcreteStateA 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
class Context {
    private var currentState: State? = null

    fun setState(state: State) {
        currentState = state
        println("Context: State changed to ${state::class.simpleName}")
    }

    fun request() {
        currentState?.handle(this) ?: println("Context: No state is set")
    }
}
  • The Context class holds a reference to the current state 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
fun main() {
    val context = Context()

    // Set initial state
    context.setState(ConcreteStateA())

    // Trigger behavior and transition between states
    context.request() // State A handles and transitions to State B
    context.request() // State B handles and transitions to State A
    context.request() // State A handles and transitions to State B
}

Output

Kotlin
Context: State changed to ConcreteStateA
State A: Handling request and transitioning to State B
Context: State changed to ConcreteStateB
State B: Handling request and transitioning to State A
Context: State changed to ConcreteStateA
State A: Handling request and transitioning to State B
Context: State changed to ConcreteStateB

How These Components Work Together

  1. The Context is the central point of interaction for the client code. It contains a reference to the current state.
  2. The State Interface ensures that all states adhere to a consistent set of behaviors.
  3. The Concrete States implement specific behavior for the Context and may trigger transitions to other states.
  4. When a client invokes a method on the Context, the Context delegates the behavior to the current state, which executes the appropriate logic.

Real-Time Use Cases

Game Development

Gun Fire Squade Battleground

The State Design Pattern is widely used in game development. A game character can exist in various states, such as healthy, surviving, or dead. In the healthy state, the character can attack enemies using different weapons. When the character enters the surviving state, its health becomes critical. Once the health reaches zero, the character transitions into the dead state, signaling the end of the game.

Now, let’s first explore how we could implement this use case without using the State Design Pattern, and then compare it with an implementation using the State Pattern for better understanding. This can be done using a series of if-else conditional checks, as demonstrated in the following code snippets.

Player Class

Kotlin
class Player {
    fun attack() {
        println("Attack")
    }

    fun fireBomb() {
        println("Fire Bomb")
    }

    fun fireGunblade() {
        println("Fire Gunblade")
    }

    fun fireLaserPistol() {
        println("Laser Pistol")
    }

    fun firePistol() {
        println("Fire Pistol")
    }

    fun survive() {
        println("Surviving!")
    }

    fun dead() {
        println("Dead! Game Over")
    }
}

Now let us define our game context class which defines the different actions conditionally depends on the state of the player.

GameContext Class (Without State Pattern)

Kotlin
class GameContext {
    private val player = Player()

    fun gameAction(state: String) {
        when (state) {
            "healthy" -> {
                player.attack()
                player.fireBomb()
                player.fireGunblade()
                player.fireLaserPistol()
            }
            "survival" -> {
                player.survive()
                player.firePistol()
            }
            "dead" -> {
                player.dead()
            }
            else -> {
                println("Invalid state")
            }
        }
    }
}

In this implementation of GameContext, we’re using a when block (or even if-else statements) to handle the state. While this approach works well for smaller examples, it can become harder to maintain and less scalable as more states and behaviors are added.

Applying the State Design Pattern

To eliminate the need for multiple conditional checks, let’s refactor the code using the State Design Pattern. We will define separate state classes for each state, and the GameContext will delegate the actions to the appropriate state object.

Define the State Interface

Kotlin
interface PlayerState {
    fun performActions(player: Player)
}

Implement Concrete States

Now, we’ll create concrete state classes for each state: HealthyState, SurvivalState, and DeadState.

Kotlin
class HealthyState : PlayerState {
    override fun performActions(player: Player) {
        player.attack()
        player.fireBomb()
        player.fireGunblade()
        player.fireLaserPistol()
    }
}

class SurvivalState : PlayerState {
    override fun performActions(player: Player) {
        player.survive()
        player.firePistol()
    }
}

class DeadState : PlayerState {
    override fun performActions(player: Player) {
        player.dead()
    }
}

Modify the GameContext Class

Finally, the GameContext class will hold a reference to the current state and delegate the action calls to that state.

Kotlin
class GameContext {
    private val player = Player()
    private var state: PlayerState = HealthyState() // Default state

    fun setState(state: PlayerState) {
        this.state = state
    }

    fun gameAction() {
        state.performActions(player)
    }
}

Testing the State Design Pattern

Now, let’s demonstrate how we can switch between different states and let the player perform actions based on the current state:

Kotlin
fun main() {
    val gameContext = GameContext()

    println("Player in Healthy state:")
    gameContext.gameAction() // Perform actions in Healthy state

    println("\nPlayer in Survival state:")
    gameContext.setState(SurvivalState())
    gameContext.gameAction() // Perform actions in Survival state

    println("\nPlayer in Dead state:")
    gameContext.setState(DeadState())
    gameContext.gameAction() // Perform actions in Dead state
}

Output

Kotlin
Player in Healthy state:
Attack
Fire Bomb
Fire Gunblade
Laser Pistol

Player in Survival state:
Surviving!
Fire Pistol

Player in Dead state:
Dead! Game Over

Let’s see what benefits we get by using the State Pattern in this case.

  • Cleaner Code: The GameContext class no longer contains any conditionals. The logic is moved to the state classes, making it easier to manage and extend.
  • Modular: Each state is encapsulated in its own class, which improves maintainability. If you need to add new states, you just need to implement a new PlayerState class.
  • Extensible: New actions or states can be added without modifying the existing code. You simply create new state classes for additional behaviors.

Before generalizing the benefits of the State Pattern, let’s look at one more use case, which is in a document editor.

A Document Workflow

We’ll create a simple document editor. A document can be in one of three states:

  1. Draft
  2. Moderation
  3. Published

The actions allowed will depend on the state:

  • In Draft, you can edit or submit for review.
  • In Moderation, you can approve or reject the document.
  • In Published, no changes are allowed.

Without further delay, let’s implement it.

Define a State Interface

The state interface defines the contract for all possible states. Each state will implement this interface.

Kotlin
// State.kt
interface State {
    fun edit(document: Document)
    fun submitForReview(document: Document)
    fun publish(document: Document)
}

Create Concrete State Classes

Each state class represents a specific state and provides its own implementation of the state behavior.

Draft State

Kotlin
// DraftState.kt
class DraftState : State {
    override fun edit(document: Document) {
        println("Editing the document...")
    }

    override fun submitForReview(document: Document) {
        println("Submitting the document for review...")
        document.changeState(ModerationState()) // Transition to Moderation
    }

    override fun publish(document: Document) {
        println("Cannot publish a document in draft state.")
    }
}

Moderation State

Kotlin
// ModerationState.kt
class ModerationState : State {
    override fun edit(document: Document) {
        println("Cannot edit a document under moderation.")
    }

    override fun submitForReview(document: Document) {
        println("Document is already under review.")
    }

    override fun publish(document: Document) {
        println("Publishing the document...")
        document.changeState(PublishedState()) // Transition to Published
    }
}

Published State

Kotlin
// PublishedState.kt
class PublishedState : State {
    override fun edit(document: Document) {
        println("Cannot edit a published document.")
    }

    override fun submitForReview(document: Document) {
        println("Cannot submit a published document for review.")
    }

    override fun publish(document: Document) {
        println("Document is already published.")
    }
}

Define the Context Class

The context class represents the object whose behavior changes with its state. It maintains a reference to the current state.

Kotlin
// Document.kt
class Document {
    private var state: State = DraftState() // Initial state

    fun changeState(newState: State) {
        state = newState
        println("Document state changed to: ${state.javaClass.simpleName}")
    }

    fun edit() = state.edit(this)
    fun submitForReview() = state.submitForReview(this)
    fun publish() = state.publish(this)
}

Test the Implementation

Finally, we can create a main function to test how our document transitions between states.

Kotlin
// Main.kt
fun main() {
    val document = Document()

    // Current state: Draft
    document.edit()
    document.submitForReview()

    // Current state: Moderation
    document.edit()
    document.publish()

    // Current state: Published
    document.submitForReview()
    document.edit()
}

Output

Kotlin
Editing the document...
Submitting the document for review...
Document state changed to: ModerationState
Cannot edit a document under moderation.
Publishing the document...
Document state changed to: PublishedState
Cannot submit a published document for review.
Cannot edit a published document.

Benefits of Using the State Design Pattern

  • Cleaner Code: Behavior is encapsulated in state-specific classes, eliminating conditionals.
  • Scalability: Adding new states is straightforward—just implement the State interface.
  • Encapsulation: Each state manages its behavior, reducing the responsibility of the context class.
  • Dynamic Behavior: The object’s behavior changes at runtime by switching states.

When Should You Use the State Pattern?

The State Pattern is ideal when:

  • An object’s behavior depends on its state.
  • You have complex conditional logic based on state.
  • States frequently change, and new states may be added.

However, avoid using it if:

  • Your application has only a few states with minimal behavior.
  • The state transitions are rare or do not justify the overhead of additional classes.

Conclusion

The State Design Pattern is an excellent way to achieve cleaner, more maintainable, and modular code. By isolating state-specific behaviors within their own classes, we can simplify the logic in our context objects and make our programs easier to extend.

In this blog, we examined the State Pattern through the lens of a document workflow and game development example. I hope this walkthrough provided clarity on implementing it in Kotlin. Now it’s your chance—experiment with this pattern in your own projects and experience its impact firsthand..!

Chain of Responsibility

Chain of Responsibility Design Pattern in Kotlin: A Detailed Guide

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. Today, we’ll dive deep into how this design pattern works and how to implement it in Kotlin.

What is the Chain of Responsibility 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?

Before we delve into the structure and implementation of the Chain of Responsibility (CoR) pattern, let’s first understand why it’s important.

Consider a situation where multiple objects are involved in processing a request, and the handling varies depending on the specific conditions. For example, in online shopping platforms like Myntra or Amazon, or food delivery services such as Zomato or Swiggy, a customer might use a discount code or coupon. The system needs to determine if the code is valid or decide which discount should apply based on the circumstances.

This is where the Chain of Responsibility pattern becomes highly useful. Rather than linking the request to a specific handler, it enables the creation of a chain of handlers, each capable of managing the request in its unique way. This makes the system more adaptable, allowing developers to easily add, remove, or modify handlers without affecting the core logic.

So, 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.

We’ll look at the actual code and explore additional real-world examples shortly to make this concept even clearer.

Structure of the Chain of Responsibility Pattern

The Chain of Responsibility pattern consists of:

  1. Handler Interface: Declares a method to process requests and optionally set the next handler.
  2. Concrete Handlers: Implements the interface and processes the request.
  3. Client Code: Creates and configures the chain.
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.

Kotlin
abstract class Handler {
    protected var nextHandler: Handler? = null
    
    abstract fun handleRequest(request: String)
    
    fun setNextHandler(handler: Handler) {
        nextHandler = handler
    }
}
  • This defines an interface for handling requests, usually with a method like handleRequest(). It may also have a reference to the next handler in the chain.
  • The handler may choose to process the request or pass it on to the next handler.

ConcreteHandler

Implement the handleRequest() method to either process the request or pass it to the next handler.

Kotlin
class ConcreteHandlerA : Handler() {
    override fun handleRequest(request: String) {
        if (request == "A") {
            println("Handler A processed request: $request")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}

class ConcreteHandlerB : Handler() {
    override fun handleRequest(request: String) {
        if (request == "B") {
            println("Handler B processed request: $request")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}
  • These are the actual handler classes that implement the handleRequest() method. Each concrete handler will either process the request or pass it to the next handler in the chain.
  • If a handler is capable of processing the request, it does so; otherwise, it forwards the request to the next handler in the chain.

Client

Interacts only with the first handler in the chain, unaware of the specific handler processing the request.

Kotlin
fun main() {
    val handlerA = ConcreteHandlerA()
    val handlerB = ConcreteHandlerB()

    handlerA.setNextHandler(handlerB)
    
    // Client sends the request to the first handler
    handlerA.handleRequest("A") // Handler A processes the request
    handlerA.handleRequest("B") // Handler B processes the request
}
  • The client sends the request to the first handler in the chain. The client does not need to know which handler will eventually process the request.

As we can see, this structure allows requests to pass through multiple handlers in the chain, with each handler having the option to process the request or delegate it.

Now, let’s roll up our sleeves and dive into real-world use case code.

Real-World Use Case

Now, let’s roll up our sleeves and dive into real-world use case code.

Handling Employee Request

Let’s revisit our employee leave request scenario, where we need to approve a leave request in a company. The leave request should be processed by different authorities depending on the amount of leave being requested. Here’s the hierarchy:

CoR in Leave Request
  • Employee: Initiates the leave request by submitting it to their immediate supervisor or system.
  • Manager (up to 5 days): Approves short leaves to handle minor requests efficiently.
  • Director (up to 15 days): Approves extended leaves, ensuring alignment with organizational policies.
  • HR (more than 15 days): Handles long-term leave requests, requiring policy compliance or special considerations.

Using the Chain of Responsibility, we can chain the approval process such that if one handler (e.g., Manager) cannot process the request, it is passed to the next handler (e.g., Director).

Define the Handler Interface

The handler interface is a blueprint for the handlers that will process requests. Each handler can either process the request or pass it along to the next handler in the chain.

Kotlin
// Create the Handler interface
interface LeaveRequestHandler {
    fun handleRequest(request: LeaveRequest)
}

In this case, the handleRequest function takes a LeaveRequest object, which holds the details of the leave request, and processes it.

Define the Request Object

The request object contains all the information related to the request. Here, we’ll create a simple LeaveRequest class.

Kotlin
// Create the LeaveRequest object
data class LeaveRequest(val employeeName: String, val numberOfDays: Int)

Create Concrete Handlers

Now, we’ll implement different concrete handlers for each authority: Manager, Director, and HR. Each handler will check if it can approve the leave request based on the number of days requested.

Kotlin
// Implement the Manager Handler
class Manager(private val nextHandler: LeaveRequestHandler? = null) : LeaveRequestHandler {
    override fun handleRequest(request: LeaveRequest) {
        if (request.numberOfDays <= 5) {
            println("Manager approved ${request.employeeName}'s leave for ${request.numberOfDays} days.")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}

// Implement the Director Handler
class Director(private val nextHandler: LeaveRequestHandler? = null) : LeaveRequestHandler {
    override fun handleRequest(request: LeaveRequest) {
        if (request.numberOfDays <= 15) {
            println("Director approved ${request.employeeName}'s leave for ${request.numberOfDays} days.")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}

// Implement the HR Handler
class HR : LeaveRequestHandler {
    override fun handleRequest(request: LeaveRequest) {
        println("HR approved ${request.employeeName}'s leave for ${request.numberOfDays} days.")
    }
}

Each handler checks whether the request can be processed. If it cannot, the request is passed to the next handler in the chain.

Set Up the Chain

Next, we’ll set up the chain of responsibility. We will link the handlers so that each handler knows who to pass the request to if it can’t handle it.

Kotlin
// Setup the chain
fun createLeaveApprovalChain(): LeaveRequestHandler {
    val hr = HR()
    val director = Director(hr)
    val manager = Manager(director)
    
    return manager // The chain starts with the Manager
}
  • If the Manager can’t approve the leave (i.e., the request is for more than 5 days), it passes the request to the Director.
  • If the Director can’t approve the leave (i.e., the request is for more than 15 days), it passes the request to HR, which will handle it.

Test the Chain of Responsibility

Now, let’s create a LeaveRequest and pass it through the chain.

Kotlin
fun main() {
    val leaveRequest = LeaveRequest("amol pawar", 10)
    
    val approvalChain = createLeaveApprovalChain()
    approvalChain.handleRequest(leaveRequest)
}

// OUTPUT

// Director approved amol pawar's leave for 10 days.

Now, let’s explore an E-Commerce Discount System using CoR.

Discount Handling in an E-Commerce System

Let’s imagine a scenario with platforms like Myntra, Flipkart, or Amazon, where we have different types of discounts:

  • Coupon Discount (10% off)
  • Member Discount (5% off)
  • Seasonal Discount (15% off)

In this case, we’ll pass a request for a discount through a chain of handlers. Each handler checks if it can process the request or passes it to the next one in the chain.

Let’s now see how we can implement the Chain of Responsibility pattern in this case.

Define the Handler Interface

The handler interface will define a method handleRequest() that every concrete handler will implement.

Kotlin
// Handler Interface
interface DiscountHandler {
    fun setNext(handler: DiscountHandler): DiscountHandler
    fun handleRequest(amount: Double): Double
}

Here, the setNext() method will allow us to chain multiple handlers together, and handleRequest() will process the request.

Concrete Handlers

Let’s define the different discount handlers. Each one will check if it can handle the discount request. If it can’t, it will forward it to the next handler.

Kotlin
// Concrete Handler for Coupon Discount
class CouponDiscountHandler(private val discount: Double) : DiscountHandler {
    private var nextHandler: DiscountHandler? = null

    override fun setNext(handler: DiscountHandler): DiscountHandler {
        nextHandler = handler
        return handler
    }

    override fun handleRequest(amount: Double): Double {
        val discountedAmount = if (discount > 0) {
            println("Applying Coupon Discount: $discount%")
            amount - (amount * (discount / 100))
        } else {
            nextHandler?.handleRequest(amount) ?: amount
        }
        return discountedAmount
    }
}

// Concrete Handler for Member Discount
class MemberDiscountHandler(private val discount: Double) : DiscountHandler {
    private var nextHandler: DiscountHandler? = null

    override fun setNext(handler: DiscountHandler): DiscountHandler {
        nextHandler = handler
        return handler
    }

    override fun handleRequest(amount: Double): Double {
        val discountedAmount = if (discount > 0) {
            println("Applying Member Discount: $discount%")
            amount - (amount * (discount / 100))
        } else {
            nextHandler?.handleRequest(amount) ?: amount
        }
        return discountedAmount
    }
}

// Concrete Handler for Seasonal Discount
class SeasonalDiscountHandler(private val discount: Double) : DiscountHandler {
    private var nextHandler: DiscountHandler? = null

    override fun setNext(handler: DiscountHandler): DiscountHandler {
        nextHandler = handler
        return handler
    }

    override fun handleRequest(amount: Double): Double {
        val discountedAmount = if (discount > 0) {
            println("Applying Seasonal Discount: $discount%")
            amount - (amount * (discount / 100))
        } else {
            nextHandler?.handleRequest(amount) ?: amount
        }
        return discountedAmount
    }
}

Client Code

Now that we have defined our concrete handlers, let’s see how the client can set up the chain and make a request.

Kotlin
fun main() {
    // Creating discount handlers
    val couponHandler = CouponDiscountHandler(10.0) // 10% off
    val memberHandler = MemberDiscountHandler(5.0) // 5% off
    val seasonalHandler = SeasonalDiscountHandler(15.0) // 15% off

    // Chain the handlers
    couponHandler.setNext(memberHandler).setNext(seasonalHandler)

    // Client request
    val originalAmount = 1000.0
    val finalAmount = couponHandler.handleRequest(originalAmount)

    println("Final Amount after applying discounts: $finalAmount")
}

// OUTPUT

// Applying Coupon Discount: 10.0%
// Final Amount after applying discounts: 900.0

Here,

  • Handlers: We define three concrete handlers, each responsible for applying a specific discount: coupon, member, and seasonal.
  • Chaining: We chain the handlers together using setNext(). The chain is set up such that if one handler can’t process the request, it passes the request to the next handler.
  • Request Processing: The client sends a request for the original amount (1000.0 in our example) to the first handler (couponHandler). If the handler can process the request, it applies the discount and returns the discounted amount. If it can’t, it forwards the request to the next handler.

But here’s the twist: what if we want to apply only the member discount? What will the output be, and how can we achieve this? Let’s see how we can do it. To achieve this, we need to set all other discounts to either 0 or negative. Only then will we get the exact output we want. 

Here, I’ll skip the actual code and just show the changes and the output.

Kotlin
    val couponHandler = CouponDiscountHandler(0.0) 
    val memberHandler = MemberDiscountHandler(5.0) // only applying 5% off
    val seasonalHandler = SeasonalDiscountHandler(-5.0) // 0 or negative 

    
    // Output 

    // Applying Member Discount: 5.0%
    // Final Amount after applying discounts: 950.0

Please note that this might seem a bit confusing, so let me clarify. In this example, we apply only one discount at a time. To achieve this, we set the other discounts to zero, ensuring that only the selected discount is applied. This is because we’ve set the starting handler to the coupon discount, which prevents the other discounts from being applied sequentially. As we often see on platforms like Flipkart and Amazon, only one coupon can typically be applied at a time.

However, this doesn’t mean that applying multiple coupons sequentially (i.e., applying all selected discounts at once) is impossible using the Chain of Responsibility (CoR) pattern. In fact, it can be achieved through the accumulation of discounts, where the total amount is updated after each sequential discount (e.g., coupon, member discount, seasonal discount). This allows us to apply all the discounts and calculate the final amount.

The sequential discount approach is particularly useful in the early stages of a business, when the goal is to attract more users or customers. In well-established e-commerce systems, however, only one discount is usually applied. In our case, we achieve this by setting the other discounts to either zero or a negative value.

Just to clarify, this explanation is a simplified scenario to help understand the concept. Real-world use cases often involve more complexities and variations. I hope this helps clear things up..!

Now, one more familiar use case that many of us have likely worked with in past projects is the Logging System.

Logging System

Let’s implement a logging system where log messages are passed through a chain of loggers. Each logger decides whether to handle the log or pass it along. We’ll include log levels: INFO, DEBUG, and ERROR.

Define a Base Logger Class

First, we’ll create an abstract Logger class. This will act as the base for all specific loggers.

Kotlin
abstract class Logger(private val level: Int) {

    private var nextLogger: Logger? = null

    // Set the next logger in the chain
    fun setNext(logger: Logger): Logger {
        this.nextLogger = logger
        return logger
    }

    // Handle the log request
    fun logMessage(level: Int, message: String) {
        if (this.level == level) {
            write(message)
            return                // To stop further propagation
        }
        nextLogger?.logMessage(level, message)
    }

    // Abstract method for handling the log
    protected abstract fun write(message: String)
}

Here,

  • level: Defines the log level this logger will handle.
  • nextLogger: Points to the next handler in the chain.
  • setNext: Configures the next logger in the chain, enabling chaining.
  • logMessage: Checks if the logger should process the message based on its level. If not, it delegates the task to the next logger in the chain.
  • return: Why used return here..? The return is used to prevent the log message from being passed to the next logger after it has been processed. It ensures that once a logger handles the message, it won’t be forwarded further, avoiding redundant output.

Create Specific Logger Implementations

Let’s create concrete loggers for INFO, DEBUG, and ERROR levels.

Kotlin
class InfoLogger : Logger(1) {
    override fun write(message: String) {
        println("INFO: $message")
    }
}

class DebugLogger : Logger(2) {
    override fun write(message: String) {
        println("DEBUG: $message")
    }
}

class ErrorLogger : Logger(3) {
    override fun write(message: String) {
        println("ERROR: $message")
    }
}
  • Each logger overrides the write method to handle messages at its respective log level.
  • For instance, the InfoLogger handles logs with a level of 1, while higher levels (DEBUG or ERROR) are passed down the chain.

Setting Up the Chain

Now, let’s set up a chain of loggers: INFO → DEBUG → ERROR.

Kotlin
fun getLoggerChain(): Logger {
    val errorLogger = ErrorLogger()
    val debugLogger = DebugLogger()
    val infoLogger = InfoLogger()

    infoLogger.setNext(debugLogger).setNext(errorLogger)

    return infoLogger
}
  • We instantiate the loggers and chain them together using setNext.
  • The chain starts with InfoLogger and ends with ErrorLogger.

Using the Logger Chain

Now, let’s test our chain.

Kotlin
fun main() {
    val loggerChain = getLoggerChain()

    println("Testing Chain of Responsibility:")
    loggerChain.logMessage(1, "This is an info message.")
    loggerChain.logMessage(2, "This is a debug message.")
    loggerChain.logMessage(3, "This is an error message.")
}

Output

Kotlin
Testing Chain of Responsibility:
INFO: This is an info message.
DEBUG: This is a debug message.
ERROR: This is an error message.

Basically, what happens here is,

  • A log with level 1 is processed by InfoLogger.
  • A log with level 2 is handled by DebugLogger.
  • A log with level 3 is processed by ErrorLogger.

If no logger in the chain can handle a request, it simply passes through without being processed.

Extending the Chain

What if we wanted to add more log levels, like TRACE? Easy..! Just implement a TraceLogger and link it to the chain without modifying the existing code.

Kotlin
class TraceLogger : Logger(0) {
    override fun write(message: String) {
        println("TRACE: $message")
    }
}

We also need to adjust the chain setup.

Kotlin
val traceLogger = TraceLogger()
traceLogger.setNext(infoLogger)

A Few More Real-Life Applications

  • Middleware in Web Frameworks: Processing HTTP requests in frameworks like Spring Boot or Ktor.
  • Authorization Systems: Sequential permission checks.
  • Form Validation: Sequential checks for input validation.
  • Payment Processing: Delegating payment methods to appropriate processors.

Advantages and Disadvantages

Advantages

  • Flexibility: Easily add or remove handlers in the chain.
  • Decoupling: The sender doesn’t know which handler processes the request.
  • Open/Closed Principle: New handlers can be added without modifying existing code.

Disadvantages

  • No Guarantee of Handling: If no handler processes the request, it may go unhandled.
  • Debugging Complexity: Long chains can be hard to trace.

Conclusion

The Chain of Responsibility pattern offers an elegant solution for delegating requests through a sequence of handlers. By decoupling the sender of a request from its receivers, this approach enhances flexibility and improves the maintainability of your code. Each handler focuses on specific tasks and passes unhandled requests to the next handler in the chain.

In this guide, we explored practical examples to illustrate the use of the Chain of Responsibility pattern in Kotlin, showcasing its application in scenarios like leave approval and discount processing. The pattern proves highly adaptable to diverse use cases, allowing for clean and organized request handling.

Incorporating this pattern into your design helps build systems that are easier to extend, maintain, and scale, ensuring they remain robust in the face of changing requirements.

Chain of Responsibility Pattern Examples

Essential Chain of Responsibility Pattern Examples: Structure and Key Insights

The Chain of Responsibility Pattern is a powerful design pattern that helps streamline the process of handling requests in a system. By allowing multiple handlers to process a request, this pattern ensures that the request is passed through a chain until it’s appropriately dealt with. In this blog, we will explore essential Chain of Responsibility Pattern examples, diving into its structure and providing key insights on how this pattern can be effectively used to create more flexible and maintainable code. Whether you’re new to design patterns or looking to expand your knowledge, this guide will help you understand the full potential of this pattern.

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.

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?

Before we delve into the structure and implementation of the Chain of Responsibility (CoR) pattern, let’s first understand why it’s important.

Consider a situation where multiple objects are involved in processing a request, and the handling varies depending on the specific conditions. For example, in online shopping platforms like Myntra or Amazon, or food delivery services such as Zomato or Swiggy, a customer might use a discount code or coupon. The system needs to determine if the code is valid or decide which discount should apply based on the circumstances.

This is where the Chain of Responsibility pattern becomes highly useful. Rather than linking the request to a specific handler, it enables the creation of a chain of handlers, each capable of managing the request in its unique way. This makes the system more adaptable, allowing developers to easily add, remove, or modify handlers without affecting the core logic.

Structure of the Chain of Responsibility Pattern

Key Idea

  • Decouple the sender and receiver.
  • Each handler in the chain determines if it can handle the request.
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.

Kotlin
abstract class Handler {
    protected var nextHandler: Handler? = null
    
    abstract fun handleRequest(request: String)
    
    fun setNextHandler(handler: Handler) {
        nextHandler = handler
    }
}
  • This defines an interface for handling requests, usually with a method like handleRequest(). It may also have a reference to the next handler in the chain.
  • The handler may choose to process the request or pass it on to the next handler.

ConcreteHandler

Implement the handleRequest() method to either process the request or pass it to the next handler.

Kotlin
class ConcreteHandlerA : Handler() {
    override fun handleRequest(request: String) {
        if (request == "A") {
            println("Handler A processed request: $request")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}

class ConcreteHandlerB : Handler() {
    override fun handleRequest(request: String) {
        if (request == "B") {
            println("Handler B processed request: $request")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}
  • These are the actual handler classes that implement the handleRequest() method. Each concrete handler will either process the request or pass it to the next handler in the chain.
  • If a handler is capable of processing the request, it does so; otherwise, it forwards the request to the next handler in the chain.

Client

Interacts only with the first handler in the chain, unaware of the specific handler processing the request.

Kotlin
fun main() {
    val handlerA = ConcreteHandlerA()
    val handlerB = ConcreteHandlerB()

    handlerA.setNextHandler(handlerB)
    
    // Client sends the request to the first handler
    handlerA.handleRequest("A") // Handler A processes the request
    handlerA.handleRequest("B") // Handler B processes the request
}
  • The client sends the request to the first handler in the chain. The client does not need to know which handler will eventually process the request.

Chain of Responsibility Pattern Examples : Real-World Use Cases

Now, let’s roll up our sleeves and dive into real-world use case code.

Handling Employee Request

Let’s revisit our employee leave request scenario, where we need to approve a leave request in a company. The leave request should be processed by different authorities depending on the amount of leave being requested. Here’s the hierarchy:

  • Employee: Initiates the leave request by submitting it to their immediate supervisor or system.
  • Manager (up to 5 days): Approves short leaves to handle minor requests efficiently.
  • Director (up to 15 days): Approves extended leaves, ensuring alignment with organizational policies.
  • HR (more than 15 days): Handles long-term leave requests, requiring policy compliance or special considerations.

Using the Chain of Responsibility, we can chain the approval process such that if one handler (e.g., Manager) cannot process the request, it is passed to the next handler (e.g., Director).

Define the Handler Interface

The handler interface is a blueprint for the handlers that will process requests. Each handler can either process the request or pass it along to the next handler in the chain.

Kotlin
// Create the Handler interface
interface LeaveRequestHandler {
    fun handleRequest(request: LeaveRequest)
}

In this case, the handleRequest function takes a LeaveRequest object, which holds the details of the leave request, and processes it.

Define the Request Object

The request object contains all the information related to the request. Here, we’ll create a simple LeaveRequest class.

Kotlin
// Create the LeaveRequest object
data class LeaveRequest(val employeeName: String, val numberOfDays: Int)

Create Concrete Handlers

Now, we’ll implement different concrete handlers for each authority: Manager, Director, and HR. Each handler will check if it can approve the leave request based on the number of days requested.

Kotlin
// Implement the Manager Handler
class Manager(private val nextHandler: LeaveRequestHandler? = null) : LeaveRequestHandler {
    override fun handleRequest(request: LeaveRequest) {
        if (request.numberOfDays <= 5) {
            println("Manager approved ${request.employeeName}'s leave for ${request.numberOfDays} days.")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}

// Implement the Director Handler
class Director(private val nextHandler: LeaveRequestHandler? = null) : LeaveRequestHandler {
    override fun handleRequest(request: LeaveRequest) {
        if (request.numberOfDays <= 15) {
            println("Director approved ${request.employeeName}'s leave for ${request.numberOfDays} days.")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}

// Implement the HR Handler
class HR : LeaveRequestHandler {
    override fun handleRequest(request: LeaveRequest) {
        println("HR approved ${request.employeeName}'s leave for ${request.numberOfDays} days.")
    }
}

Each handler checks whether the request can be processed. If it cannot, the request is passed to the next handler in the chain.

Set Up the Chain

Next, we’ll set up the chain of responsibility. We will link the handlers so that each handler knows who to pass the request to if it can’t handle it.

Kotlin
// Setup the chain
fun createLeaveApprovalChain(): LeaveRequestHandler {
    val hr = HR()
    val director = Director(hr)
    val manager = Manager(director)
    
    return manager // The chain starts with the Manager
}
  • If the Manager can’t approve the leave (i.e., the request is for more than 5 days), it passes the request to the Director.
  • If the Director can’t approve the leave (i.e., the request is for more than 15 days), it passes the request to HR, which will handle it.

Test the Chain of Responsibility

Now, let’s create a LeaveRequest and pass it through the chain.

Kotlin
fun main() {
    val leaveRequest = LeaveRequest("amol pawar", 10)
    
    val approvalChain = createLeaveApprovalChain()
    approvalChain.handleRequest(leaveRequest)
}

// OUTPUT

// Director approved amol pawar's leave for 10 days.

Now, one more familiar use case that many of us have likely worked with in past projects is the Logging System.

Logging System

Let’s implement a logging system where log messages are passed through a chain of loggers. Each logger decides whether to handle the log or pass it along. We’ll include log levels: INFO, DEBUG, and ERROR.

Define a Base Logger Class

First, we’ll create an abstract Logger class. This will act as the base for all specific loggers.

Kotlin
abstract class Logger(private val level: Int) {

    private var nextLogger: Logger? = null

    // Set the next logger in the chain
    fun setNext(logger: Logger): Logger {
        this.nextLogger = logger
        return logger
    }

    // Handle the log request
    fun logMessage(level: Int, message: String) {
        if (this.level == level) {
            write(message)
            return                // To stop further propagation
        }
        nextLogger?.logMessage(level, message)
    }

    // Abstract method for handling the log
    protected abstract fun write(message: String)
}

Here,

  • level: Defines the log level this logger will handle.
  • nextLogger: Points to the next handler in the chain.
  • setNext: Configures the next logger in the chain, enabling chaining.
  • logMessage: Checks if the logger should process the message based on its level. If not, it delegates the task to the next logger in the chain.
  • return: Why used return here..? The return is used to prevent the log message from being passed to the next logger after it has been processed. It ensures that once a logger handles the message, it won’t be forwarded further, avoiding redundant output.

Create Specific Logger Implementations

Let’s create concrete loggers for INFO, DEBUG, and ERROR levels.

Kotlin
class InfoLogger : Logger(1) {
    override fun write(message: String) {
        println("INFO: $message")
    }
}

class DebugLogger : Logger(2) {
    override fun write(message: String) {
        println("DEBUG: $message")
    }
}

class ErrorLogger : Logger(3) {
    override fun write(message: String) {
        println("ERROR: $message")
    }
}
  • Each logger overrides the write method to handle messages at its respective log level.
  • For instance, the InfoLogger handles logs with a level of 1, while higher levels (DEBUG or ERROR) are passed down the chain.

Setting Up the Chain

Now, let’s set up a chain of loggers: INFO → DEBUG → ERROR.

Kotlin
fun getLoggerChain(): Logger {
    val errorLogger = ErrorLogger()
    val debugLogger = DebugLogger()
    val infoLogger = InfoLogger()

    infoLogger.setNext(debugLogger).setNext(errorLogger)

    return infoLogger
}
  • We instantiate the loggers and chain them together using setNext.
  • The chain starts with InfoLogger and ends with ErrorLogger.

Using the Logger Chain

Now, let’s test our chain.

Kotlin
fun main() {
    val loggerChain = getLoggerChain()

    println("Testing Chain of Responsibility:")
    loggerChain.logMessage(1, "This is an info message.")
    loggerChain.logMessage(2, "This is a debug message.")
    loggerChain.logMessage(3, "This is an error message.")
}

Output

Kotlin
Testing Chain of Responsibility:
INFO: This is an info message.
DEBUG: This is a debug message.
ERROR: This is an error message.

Basically, what happens here is,

  • A log with level 1 is processed by InfoLogger.
  • A log with level 2 is handled by DebugLogger.
  • A log with level 3 is processed by ErrorLogger.

If no logger in the chain can handle a request, it simply passes through without being processed.

Extending the Chain

What if we wanted to add more log levels, like TRACE? Easy..! Just implement a TraceLogger and link it to the chain without modifying the existing code.

Kotlin
class TraceLogger : Logger(0) {
    override fun write(message: String) {
        println("TRACE: $message")
    }
}

We also need to adjust the chain setup.

Kotlin
val traceLogger = TraceLogger()
traceLogger.setNext(infoLogger)

Some Other Real-Life Applications

  • Middleware in Web Frameworks: Processing HTTP requests in frameworks like Spring Boot or Ktor.
  • Authorization Systems: Sequential permission checks.
  • Form Validation: Sequential checks for input validation.
  • Payment Processing: Delegating payment methods to appropriate processors.

Advantages and Disadvantages

Advantages

  • Flexibility: Easily add or remove handlers in the chain.
  • Decoupling: The sender doesn’t know which handler processes the request.
  • Open/Closed Principle: New handlers can be added without modifying existing code.

Disadvantages

  • No Guarantee of Handling: If no handler processes the request, it may go unhandled.
  • Debugging Complexity: Long chains can be hard to trace.

Cocnlusion

The Chain of Responsibility Pattern offers a flexible and scalable solution to handling requests, making it an essential tool for developers looking to decouple their system’s components. Through the examples and insights shared, it becomes clear how this pattern can simplify complex workflows and enhance maintainability. By mastering the structure and application of the Chain of Responsibility pattern, you can ensure your systems are more adaptable to future changes, ultimately improving both code quality and overall project efficiency.

Chain of Responsibility Pattern Structure

Unveiling the Powerful Chain of Responsibility Pattern Structure: Key Components and How It Works

The Chain of Responsibility pattern is a behavioral design pattern that allows a request to be passed along a chain of handlers until it is processed. This pattern is particularly powerful in scenarios where multiple objects can process a request, but the handler is not determined until runtime. In this blog, we will be unveiling the powerful structure of the Chain of Responsibility pattern, breaking down its key components and flow. By the end, you’ll have a solid understanding of how this pattern can improve flexibility and scalability in your application design.

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.

Why Use the Chain of Responsibility Pattern?

Before we delve into the structure and implementation of the Chain of Responsibility (CoR) pattern, let’s first understand why it’s important.

Consider a situation where multiple objects are involved in processing a request, and the handling varies depending on the specific conditions. For example, in online shopping platforms like Myntra or Amazon, or food delivery services such as Zomato or Swiggy, a customer might use a discount code or coupon. The system needs to determine if the code is valid or decide which discount should apply based on the circumstances.

This is where the Chain of Responsibility pattern becomes highly useful. Rather than linking the request to a specific handler, it enables the creation of a chain of handlers, each capable of managing the request in its unique way. This makes the system more adaptable, allowing developers to easily add, remove, or modify handlers without affecting the core logic.

So, 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.

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.

Structure of the Chain of Responsibility Pattern

The Chain of Responsibility pattern consists of:

  1. Handler Interface: Declares a method to process requests and optionally set the next handler.
  2. Concrete Handlers: Implements the interface and processes the request.
  3. Client Code: Creates and configures the chain.
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.

Kotlin
abstract class Handler {
    protected var nextHandler: Handler? = null
    
    abstract fun handleRequest(request: String)
    
    fun setNextHandler(handler: Handler) {
        nextHandler = handler
    }
}
  • This defines an interface for handling requests, usually with a method like handleRequest(). It may also have a reference to the next handler in the chain.
  • The handler may choose to process the request or pass it on to the next handler.

ConcreteHandler

Implement the handleRequest() method to either process the request or pass it to the next handler.

Kotlin
class ConcreteHandlerA : Handler() {
    override fun handleRequest(request: String) {
        if (request == "A") {
            println("Handler A processed request: $request")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}

class ConcreteHandlerB : Handler() {
    override fun handleRequest(request: String) {
        if (request == "B") {
            println("Handler B processed request: $request")
        } else {
            nextHandler?.handleRequest(request)
        }
    }
}
  • These are the actual handler classes that implement the handleRequest() method. Each concrete handler will either process the request or pass it to the next handler in the chain.
  • If a handler is capable of processing the request, it does so; otherwise, it forwards the request to the next handler in the chain.

Client

Interacts only with the first handler in the chain, unaware of the specific handler processing the request.

Kotlin
fun main() {
    val handlerA = ConcreteHandlerA()
    val handlerB = ConcreteHandlerB()

    handlerA.setNextHandler(handlerB)
    
    // Client sends the request to the first handler
    handlerA.handleRequest("A") // Handler A processes the request
    handlerA.handleRequest("B") // Handler B processes the request
}
  • The client sends the request to the first handler in the chain. The client does not need to know which handler will eventually process the request.

As we can see, this structure allows requests to pass through multiple handlers in the chain, with each handler having the option to process the request or delegate it.

Advantages and Disadvantages

Advantages

  • Flexibility: Easily add or remove handlers in the chain.
  • Decoupling: The sender doesn’t know which handler processes the request.
  • Open/Closed Principle: New handlers can be added without modifying existing code.

Disadvantages

  • No Guarantee of Handling: If no handler processes the request, it may go unhandled.
  • Debugging Complexity: Long chains can be hard to trace.

Conclusion

The Chain of Responsibility pattern offers a robust solution for handling requests in a decoupled and flexible way, allowing for easier maintenance and scalability. By understanding the structure and key components of this pattern, you can effectively apply it to scenarios where multiple handlers are required to process requests in a dynamic and streamlined manner. Whether you’re developing complex systems or optimizing existing architectures, this pattern is a valuable tool that can enhance the efficiency and adaptability of your software design.

Design Principles

Top 4 Essential Design Principles: SOLID, DRY, KISS, and YAGNI Explained in Kotlin

Every developer wants code that’s simple to understand, easy to update, and built to last. But achieving this isn’t always easy! Thankfully, a few core principles—SOLID, DRY, KISS, and YAGNI—can help guide the way. Think of these as your trusty shortcuts to writing code that’s not only easy on the eyes but also a breeze to maintain and scale.

In Kotlin, these principles fit naturally, thanks to the language’s clean and expressive style. In this blog, we’ll explore each one with examples to show how they can make your code better without making things complicated. Ready to make coding a little easier? Let’s get started!

Introduction to Design Principles

Design principles serve as guidelines to help developers create code that’s flexible, reusable, and robust. They are essential in reducing technical debt, maintaining code quality, and ensuring ease of collaboration within teams.

Let’s dive into each principle in detail.

SOLID Principles

The SOLID principles are a collection of five design principles introduced by Robert C. Martin (Uncle Bob) that make software design more understandable, flexible, and maintainable.

S – Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change. Each class should focus on a single responsibility or task.

This principle prevents classes from becoming too complex and difficult to manage. Each class should focus on a single task, making it easy to understand and maintain.

Let’s say we have a class that both processes user data and saves it to a database.

Kotlin
// Violates SRP: Does multiple things
class UserProcessor {
    fun processUser(user: User) {
        // Logic to process user
    }

    fun saveUser(user: User) {
        // Logic to save user to database
    }
}

Solution: Split responsibilities by creating two separate classes.

Kotlin
class UserProcessor {
    fun process(user: User) {
        // Logic to process user
    }
}

class UserRepository {
    fun save(user: User) {
        // Logic to save user to database
    }
}

Now, UserProcessor only processes users, while UserRepository only saves them, adhering to SRP.

Let’s consider one more example: suppose we have an Invoice class. If we mix saving the invoice and sending it by email, this class will have more than one responsibility, violating the Single Responsibility Principle (SRP). Here’s how we can fix it:

Kotlin
class Invoice {
    fun calculateTotal() {
        // Logic to calculate total
    }
}

class InvoiceSaver {
    fun save(invoice: Invoice) {
        // Logic to save invoice to database
    }
}

class EmailSender {
    fun sendInvoice(invoice: Invoice) {
        // Logic to send invoice via email
    }
}

Here, the Invoice class focuses solely on managing invoice data. The InvoiceSaver class takes care of saving invoices, while EmailSender handles sending them via email. This separation makes the code easier to modify and test, as each class has a single responsibility.

O – Open/Closed Principle (OCP)

Definition: Classes should be open for extension but closed for modification.

This means that you should be able to add new functionality without changing existing code. In Kotlin, this can often be achieved using inheritance or interfaces.

Imagine we have a Notification class that sends email notifications. Later, we may need to add SMS notifications.

Kotlin
// Violates OCP: Modifying the class each time a new notification type is needed
class Notification {
    fun sendEmail(user: User) {
        // Email notification logic
    }
}

Solution: Use inheritance to allow extending the notification types without modifying existing code.

Kotlin
interface Notifier {
    fun notify(user: User)
}

class EmailNotifier : Notifier {
    override fun notify(user: User) {
        // Email notification logic
    }
}

class SmsNotifier : Notifier {
    override fun notify(user: User) {
        // SMS notification logic
    }
}

In this scenario, the Notifier can be easily extended without changing any existing classes, which perfectly aligns with the Open/Closed Principle (OCP). To illustrate this further, imagine we have a PaymentProcessor class. If we want to introduce new payment types without altering the current code, using inheritance or interfaces would be a smart approach.

Kotlin
interface Payment {
    fun pay()
}

class CreditCardPayment : Payment {
    override fun pay() {
        println("Paid with credit card.")
    }
}

class PayPalPayment : Payment {
    override fun pay() {
        println("Paid with PayPal.")
    }
}

class PaymentProcessor {
    fun processPayment(payment: Payment) {
        payment.pay()
    }
}

With this setup, adding a new payment type, such as CryptoPayment, is straightforward. We simply create a new class that implements the Payment interface, and there’s no need to modify the existing PaymentProcessor class. This approach perfectly adheres to the Open/Closed Principle (OCP).

L – Liskov Substitution Principle (LSP)

Note: Many of us misunderstand this concept or do not fully grasp it. Many developers believe that LSP is similar to dynamic polymorphism, but this is not entirely true, as they often overlook the key part of the LSP definition: ‘without altering the correctness of the program.’

Definition: Subtypes must be substitutable for their base types without altering the correctness of the program. This means that if a program uses a base type, it should be able to work with any of its subtypes without unexpected behavior or errors.

The Liskov Substitution Principle (LSP) ensures that subclasses can replace their parent classes while maintaining the expected behavior of the program. Violating LSP can lead to unexpected bugs and issues, as subclasses may not conform to the behaviors defined by their parent classes.

Let’s understand this with an example: Consider a Vehicle class with a drive function. If we create a Bicycle subclass, it might violate LSP because bicycles don’t “drive” in the same way cars do.

Kotlin
// Violates LSP: Bicycle shouldn't be a subclass of Vehicle
open class Vehicle {
    open fun drive() {
        // Default drive logic
    }
}

class Car : Vehicle() {
    override fun drive() {
        // Car-specific drive logic
    }
}

class Bicycle : Vehicle() {
    override fun drive() {
        throw UnsupportedOperationException("Bicycles cannot drive like cars")
    }

    fun pedal() {
        // Pedal logic
    }
}

In this example, Bicycle violates LSP because it cannot fulfill the contract of the drive method defined in Vehicle, leading to an exception when invoked.

Solution: To respect LSP, we can separate the hierarchy into interfaces that accurately represent the behavior of each type. Here’s how we can implement this:

Kotlin
interface Drivable {
    fun drive()
}

class Car : Drivable {
    override fun drive() {
        // Car-specific drive logic
    }
}

class Bicycle {
    fun pedal() {
        // Pedal logic
    }
}

Now, Car implements the Drivable interface, providing a proper implementation for drive(). The Bicycle class does not implement Drivable, as it doesn’t need to drive. Each class behaves correctly according to its nature, adhering to the Liskov Substitution Principle.

One more thing I want to add: suppose we have an Animal class and a Bird subclass.

Kotlin
open class Animal {
    open fun move() {
        println("Animal moves")
    }
}

class Bird : Animal() {
    override fun move() {
        println("Bird flies")
    }
}

In this example, Bird can replace Animal without any issues because it properly fulfills the expected behavior of the move function. When move is called on a Bird object, it produces the output “Bird flies,” which is a valid extension of the behavior defined by Animal.

This illustrates the Liskov Substitution Principle: any class inheriting from Animal should be able to act like an Animal, maintaining the expected interface and behavior.

Additional Consideration: To ensure adherence to LSP, all subclasses must conform to the expectations set by the superclass. For example, if another subclass, such as Fish, is created but its implementation of move behaves in a way that contradicts the Animal contract, it would violate LSP.

I — Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on methods they do not use. In other words, a class should not be required to implement interfaces it doesn’t need.

ISP suggests creating specific interfaces that are relevant to the intended functionality, rather than forcing a class to implement unnecessary methods.

Think about a real-world software development scenario: if there’s a Worker interface that requires both code and test methods, then a Manager class would have to implement both—even if all it really does is manage the team.

Kotlin
// Violates ISP: Manager doesn't need to code
interface Worker {
    fun code()
    fun test()
}

class Developer : Worker {
    override fun code() {
        // Coding logic
    }

    override fun test() {
        // Testing logic
    }
}

class Manager : Worker {
    override fun code() {
        // Not applicable
    }

    override fun test() {
        // Not applicable
    }
}

Solution: Split Worker into separate interfaces.

Kotlin
interface Coder {
    fun code()
}

interface Tester {
    fun test()
}

class Developer : Coder, Tester {
    override fun code() { /* Coding logic */ }
    override fun test() { /* Testing logic */ }
}

class Manager {
    // Manager specific logic
}

This way, each class only implements what it actually needs, staying true to the Interface Segregation Principle (ISP).

Here’s another example: let’s say we have a Worker interface with methods for both daytime and nighttime work.

Kotlin
interface Worker {
    fun work()
    fun nightWork()
}

class DayWorker : Worker {
    override fun work() {
        println("Day worker works")
    }
    override fun nightWork() {
        throw UnsupportedOperationException("Day worker doesn't work at night")
    }
}

Refactoring to Follow the Interface Segregation Principle (ISP):

Kotlin
interface DayShift {
    fun work()
}

interface NightShift {
    fun nightWork()
}

class DayWorker : DayShift {
    override fun work() {
        println("Day worker works")
    }
}

By splitting up the interfaces, we make sure that DayWorker only has the methods it actually needs, keeping the code simpler and reducing the chances of errors.

D — Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

DIP encourages loose coupling by focusing on dependencies being based on abstractions, not on concrete implementations.

Let’s break this down: if OrderService directly depends on EmailService, any change in the email logic will also impact OrderService.

Kotlin
// Violates DIP: Tight coupling between OrderService and EmailService
class EmailService {
    fun sendEmail(order: Order) { /* Email logic */ }
}

class OrderService {
    private val emailService = EmailService()

    fun processOrder(order: Order) {
        emailService.sendEmail(order)
    }
}

Solution: Use an abstraction for dependency injection to keep things flexible.

Kotlin
interface NotificationService {
    fun notify(order: Order)
}

class EmailService : NotificationService {
    override fun notify(order: Order) { /* Email logic */ }
}

class OrderService(private val notificationService: NotificationService) {
    fun processOrder(order: Order) {
        notificationService.notify(order)
    }
}

Now, OrderService depends on NotificationService, an abstraction, instead of directly depending on the concrete EmailService.

Here’s another use case: let’s take a look at a simple data fetching mechanism in mobile applications.

Kotlin
interface DataRepository {
    fun fetchData(): String
}

class RemoteRepository : DataRepository {
    override fun fetchData() = "Data from remote"
}

class LocalRepository : DataRepository {
    override fun fetchData() = "Data from local storage"
}

class DataService(private val repository: DataRepository) {
    fun getData(): String {
        return repository.fetchData()
    }
}

By injecting DataRepository, DataService depends on an abstraction. We can now easily switch between RemoteRepository and LocalRepository.


Other Core Principles

Beyond SOLID, let’s look at three more principles often used to simplify and improve code.

DRY Principle – Don’t Repeat Yourself

Definition: Avoid code duplication. Each piece of logic should exist in only one place. Instead of repeating code, reuse functionality through methods, classes, or functions.

Let’s say we want to calculate discounts in different parts of the application.

Kotlin
fun calculateDiscount(price: Double, discountPercentage: Double): Double {
    return price - (price * discountPercentage / 100)
}

// Reuse in other parts
val discountedPrice = calculateDiscount(100.0, 10.0)

Instead of duplicating the discount calculation logic, we use a reusable function. This makes the code cleaner and easier to maintain.

We can see another example where we extract reusable logic in one place and reuse it wherever needed.

Kotlin
// Violates DRY: Repeated logic in each function
fun addUser(name: String, email: String) {
    if (name.isNotBlank() && email.contains("@")) {
        // Add user logic
    }
}

fun updateUser(name: String, email: String) {
    if (name.isNotBlank() && email.contains("@")) {
        // Update user logic
    }
}

Solution: Extract repeated logic into a helper function.

Kotlin
fun validateUser(name: String, email: String): Boolean {
    return name.isNotBlank() && email.contains("@")
}

fun addUser(name: String, email: String) {
    if (validateUser(name, email)) {
        // Add user logic
    }
}

fun updateUser(name: String, email: String) {
    if (validateUser(name, email)) {
        // Update user logic
    }
}

Now, the validation logic is centralized, following DRY.

KISS Principle – Keep It Simple, Stupid

Definition: Keep things simple. Avoid overcomplicating things and make the code as easy to understand as possible.

Let’s say we have a function to check if a number is even.

Kotlin
// Overcomplicated
fun isEven(number: Int): Boolean {
    return if (number % 2 == 0) true else false
}

// Simplified
fun isEven(number: Int): Boolean = number % 2 == 0

By removing unnecessary logic, we keep the function short and easier to understand.

Let’s look at one more example that violates the KISS principle.

Kotlin
// Violates KISS: Overly complicated logic
fun calculateDiscount(price: Double): Double {
    return if (price > 100) {
        price * 0.1
    } else if (price > 50) {
        price * 0.05
    } else {
        0.0
    }
}

Solution: Simplify with clear logic.

Kotlin
fun calculateDiscount(price: Double): Double = when {
    price > 100 -> price * 0.1
    price > 50 -> price * 0.05
    else -> 0.0
}

Here, the when expression simplifies the code while achieving the same result.

YAGNI Principle – You Aren’t Gonna Need It

Definition: Don’t add functionality until you really need it. Only add features when they’re actually required, not just because you think you might need them later.

Imagine we’re building a calculator and think about adding a sin function, even though the requirements only need addition and subtraction.

Kotlin
class Calculator {
    fun add(a: Int, b: Int): Int = a + b
    fun subtract(a: Int, b: Int): Int = a - b
    // Avoid adding unnecessary functions like sin() unless required
}

Here, we adhere to YAGNI by only implementing what’s needed. Extra functionality can lead to complex maintenance and a bloated codebase.

Another example of violating YAGNI is when we add functionality to the user manager that we don’t actually need.

Kotlin
// Violates YAGNI: Adding unused functionality
class UserManager {
    fun getUser(id: Int) { /* Logic */ }
    fun updateUser(id: Int) { /* Logic */ }
    fun deleteUser(id: Int) { /* Logic */ }
    fun archiveUser(id: Int) { /* Logic */ } // Not required yet
}

Solution: Only implement what is required now.

Kotlin
class UserManager {
    fun getUser(id: Int) { /* Logic */ }
    fun updateUser(id: Int) { /* Logic */ }
    fun deleteUser(id: Int) { /* Logic */ }
}

The archiveUser method can be added later if needed, that way we’re following the YAGNI principle.

Conclusion

To wrap it up, following design principles like SOLID, DRY, KISS, and YAGNI can make a huge difference in the quality of your code. They help you write cleaner, more maintainable, and less error-prone code, making life easier for you and anyone else who works with it. Kotlin’s clear and expressive syntax is a great fit for applying these principles, so you can keep your code simple, efficient, and easy to understand. Stick to these principles, and your code will be in great shape for the long haul!

Liskov Substitution Principle

Many of us Misunderstand the Liskov Substitution Principle – Let’s Unfold Everything and Master LSP

In the realm of object-oriented programming, designing robust and maintainable systems is paramount. One of the foundational principles that help achieve this is the Liskov Substitution Principle (LSP). If you’ve ever dealt with class hierarchies, you’ve likely encountered situations where substitutability can lead to confusion or errors. In this blog post, we’ll break down the Liskov Substitution Principle, understand its importance, and see how to implement it effectively using Kotlin.

What is the Liskov Substitution Principle?

The Liskov Substitution Principle, named after Barbara Liskov who introduced it in 1987, states that:

If S is a subtype of T, then objects of type T should be replaceable with objects of type S without affecting the correctness of the program.

In simple words, a subclass should work in place of its superclass without causing any problems. This helps us avoid mistakes and makes our code easier to expand without bugs. For example, if you have a class Bird and a subclass Penguin, you should be able to use Penguin anywhere you use Bird without issues.

Why is Liskov Substitution Principle Important?

  1. Promotes Code Reusability: Following LSP allows developers to create interchangeable classes, enhancing reusability and reducing code duplication.
  2. Enhances Maintainability: When subclasses adhere to LSP, the code becomes easier to understand and maintain, as the relationships between classes are clearer.
  3. Reduces Bugs: By ensuring that subclasses can stand in for their parent classes, LSP helps to minimize bugs that arise from unexpected behaviors when substituting class instances.

Real-World LSP Example: Shapes

Let’s dive into an example involving shapes to illustrate LSP clearly. We’ll start by designing a base class and its subclasses, and then we’ll analyze whether the design adheres to the Liskov Substitution Principle.

The Base Class

First, we create a base class called Shape that has an abstract method for calculating the area:

Kotlin
// Shape.kt
abstract class Shape {
    abstract fun area(): Double
}

Subclasses of Shape

Now, let’s create two subclasses: Rectangle and Square.

Kotlin
// Rectangle.kt
class Rectangle(private val width: Double, private val height: Double) : Shape() {
    override fun area(): Double {
        return width * height
    }
}

// Square.kt
class Square(private val side: Double) : Shape() {
    override fun area(): Double {
        return side * side
    }
}

Using the Shapes

Next, let’s create a function to calculate the area of a shape, demonstrating how we can use both Rectangle and Square interchangeably.

Kotlin
// Main.kt
fun calculateArea(shape: Shape): Double {
    return shape.area()
}

fun main() {
    val rectangle = Rectangle(5.0, 3.0)
    val square = Square(4.0)

    println("Rectangle area: ${calculateArea(rectangle)}") // Output: 15.0
    println("Square area: ${calculateArea(square)}") // Output: 16.0
}

Now, let’s analyze: Does it follow the Liskov Substitution Principle (LSP)?

In the above code, both Rectangle and Square can be used wherever Shape is expected, and they produce correct results. This adheres to the Liskov Substitution Principle, as substituting a Shape with a Rectangle or Square doesn’t affect the program’s correctness.

Violating LSP: A Cautionary Tale

Now, let’s explore a scenario where we might inadvertently violate LSP. Imagine if we tried to implement a Square as a subclass of Rectangle:

Kotlin
// Square2.kt (Incorrect Implementation: For illustrative purposes only)
class Square2(side: Double) : Rectangle(side, side) {
    // This violates the LSP
}

Here, we try to treat Square as a special type of Rectangle. While this might seem convenient, it can cause issues, especially if we later try to set the width and height separately.

Kotlin
fun main() {
    val square = Square2(4.0)
    square.width = 5.0 // This could cause unexpected behavior
}
Leads to bugs and unexpected behavior

By trying to force a square to be a rectangle, we create scenarios where our expectations of behavior break down, violating LSP.

A Better Approach: Interfaces

To adhere to LSP more effectively, we can use interfaces instead of inheritance for our shapes:

Kotlin
// ShapeInterface.kt
interface Shape {
    fun area(): Double
}

// Rectangle.kt
class Rectangle(private val width: Double, private val height: Double) : Shape {
    override fun area(): Double {
        return width * height
    }
}

// Square.kt
class Square(private val side: Double) : Shape {
    override fun area(): Double {
        return side * side
    }
}

With this approach, we can freely create different shapes while ensuring they all adhere to the contract specified by the Shape interface.


Note: Many of us misunderstand this concept or do not fully grasp it. Many developers believe that LSP is similar to dynamic polymorphism, but this is not entirely true, as they often overlook the key part of the LSP definition: ‘without altering the correctness of the program.’

Definition: Subtypes must be substitutable for their base types without altering the correctness of the program. This means that if a program uses a base type, it should be able to work with any of its subtypes without unexpected behavior or errors.

The Liskov Substitution Principle (LSP) ensures that subclasses can replace their parent classes while maintaining the expected behavior of the program. Violating LSP can lead to unexpected bugs and issues, as subclasses may not conform to the behaviors defined by their parent classes.

Let’s understand this with a few more examples: Consider a Vehicle class with a drive function. If we create a Bicycle subclass, it may violate the Liskov Substitution Principle (LSP) because bicycles don’t ‘drive’ in the same way that cars do.

Kotlin
// Violates LSP: Bicycle shouldn't be a subclass of Vehicle
open class Vehicle {
    open fun drive() {
        // Default drive logic
    }
}

class Car : Vehicle() {
    override fun drive() {
        // Car-specific drive logic
    }
}

class Bicycle : Vehicle() {
    override fun drive() {
        throw UnsupportedOperationException("Bicycles cannot drive like cars")
    }

    fun pedal() {
        // Pedal logic
    }
}

In this example, Bicycle violates LSP because it cannot fulfill the contract of the drive method defined in Vehicle, leading to an exception when invoked.

Solution: To respect LSP, we can separate the hierarchy into interfaces that accurately represent the behavior of each type. Here’s how we can implement this:

Kotlin
interface Drivable {
    fun drive()
}

class Car : Drivable {
    override fun drive() {
        // Car-specific drive logic
    }
}

class Bicycle {
    fun pedal() {
        // Pedal logic
    }
}

Now, Car implements the Drivable interface, providing a proper implementation for drive(). The Bicycle class does not implement Drivable, as it doesn’t need to drive. Each class behaves correctly according to its nature, adhering to the Liskov Substitution Principle.

One more thing I want to add: suppose we have an Animal class and a Bird subclass.

Kotlin
open class Animal {
    open fun move() {
        println("Animal moves")
    }
}

class Bird : Animal() {
    override fun move() {
        println("Bird flies")
    }
}

In this example, Bird can replace Animal without any issues because it properly fulfills the expected behavior of the move function. When move is called on a Bird object, it produces the output “Bird flies,” which is a valid extension of the behavior defined by Animal.

This illustrates the Liskov Substitution Principle: any class inheriting from Animal should be able to act like an Animal, maintaining the expected interface and behavior.

Additional Consideration: To ensure adherence to LSP, all subclasses must conform to the expectations set by the superclass. For example, if another subclass, such as Fish, is created but its implementation of move behaves in a way that contradicts the Animal contract, it would violate LSP.


How to Avoid Violating LSP

  • Use interfaces or abstract classes that define behavior and allow different implementations.
  • Ensure that method signatures and expected behaviors remain consistent across subclasses.
  • Consider using composition over inheritance to avoid inappropriate subclassing.

Best Practices for Implementing LSP

  • Design Interfaces Thoughtfully: Design interfaces or base classes to capture only the behavior that all subclasses should have.
  • Avoid Overriding Behavior: When a method in a subclass changes expected behavior, it often signals a design issue.
  • Use Composition: When two classes share some behavior but have different constraints, use composition rather than inheritance.

Conclusion

The Liskov Substitution Principle is a fundamental concept that enhances the design of object-oriented systems. By ensuring that subclasses can be substituted for their parent classes without affecting program correctness, we create code that is more robust, maintainable, and reusable.

When designing your classes, always ask yourself: Can this subclass be used interchangeably with its parent class without altering expected behavior? If the answer is no, it’s time to reconsider your design.

Embracing LSP not only helps you write better code but also fosters a deeper understanding of your application’s architecture. So, the next time you’re faced with a class hierarchy, keep the Liskov Substitution Principle in mind, and watch your code transform into a cleaner, more maintainable version of itself!

Happy coding with LSP!

error: Content is protected !!