Design Patterns

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!

Proxy Design Pattern

The Proxy Design Pattern in Kotlin: A Comprehensive Guide

The Proxy design pattern is a structural pattern that acts as a stand-in or “placeholder” for another object, helping control access to it. By applying this pattern, you add an extra layer that manages an object’s behavior, all without altering the original object. In this article, we’ll explore the basics of the Proxy pattern, dive into real-world examples where it proves useful, and guide you through a Kotlin implementation with step-by-step explanations to make everything clear and approachable.

Proxy Design Pattern

In programming, objects sometimes need additional layers of control—whether it’s for performance optimizations, access restrictions, or simplifying complex operations. The Proxy pattern achieves this by creating a “proxy” class that represents another class. This proxy class controls access to the original class and can be used to introduce additional logic before or after the actual operations.

It’s particularly useful in situations where object creation is resource-intensive, or you want finer control over how and when the object interacts with other parts of the system. In Android, proxies are commonly used for lazy loading, network requests, and logging.

When to Use Proxy Pattern

The Proxy pattern is beneficial when:

  1. You want to control access to an object.
  2. You need to defer object initialization (lazy initialization).
  3. You want to add functionalities, like caching or logging, without modifying the actual object.

Structure of Proxy Pattern

The Proxy Pattern involves three main components:

  1. Subject Interface – Defines the common interface that both the RealSubject and Proxy implement.
  2. RealSubject – The actual object being represented or accessed indirectly.
  3. Proxy – Controls access to the RealSubject.

Types of Proxies

Different types of proxies serve distinct purposes:

  • Remote Proxy: Manages resources in remote systems.
  • Virtual Proxy: Manages resource-heavy objects and instantiates them only when needed.
  • Protection Proxy: Controls access based on permissions.
  • Cache Proxy: Caches responses to improve performance.

Real-World Use Cases

The Proxy pattern is widely used in scenarios such as:

  • Virtual Proxies: Delaying object initialization (e.g., for memory-heavy objects).
  • Protection Proxies: Adding security layers to resources (e.g., restricting access).
  • Remote Proxies: Representing objects that are in different locations (e.g., APIs).

Let’s consider a video streaming scenario, similar to popular platforms like Disney+ Hotstar, Netflix, or Amazon Prime. Here, a proxy can be used to control access to video data based on the user’s subscription type. For instance, the proxy could restrict access to premium content for free-tier users, ensuring that only eligible users can stream certain videos. This adds a layer of control, enhancing security and user experience while keeping the main video service logic clean and focused.

Here’s the simple proxy implementation,

First we define the interface

Kotlin
interface VideoService {
    fun streamVideo(videoId: String): String
}

Create the Real Object

The RealVideoService class represents the actual video streaming service. It implements the VideoService interface and streams video.

Kotlin
class RealVideoService : VideoService {
    override fun streamVideo(videoId: String): String {
        // Simulate streaming a large video file
        return "Streaming video content for video ID: $videoId"
    }
}

Create the Proxy Class

The VideoServiceProxy class controls access to the RealVideoService. We’ll implement it so only premium users can access certain videos, adding a layer of security.

Kotlin
class VideoServiceProxy(private val isPremiumUser: Boolean) : VideoService {

    // Real service reference
    private val realVideoService = RealVideoService()

    override fun streamVideo(videoId: String): String {
        return if (isPremiumUser) {
            // Delegate call to real object if the user is premium
            realVideoService.streamVideo(videoId)
        } else {
            // Restrict access for non-premium users
            "Upgrade to premium to stream this video."
        }
    }
}

Testing the Proxy

Now, let’s simulate clients trying to stream a video through the proxy.

Kotlin
fun main() {
    val premiumUserProxy = VideoServiceProxy(isPremiumUser = true)
    val regularUserProxy = VideoServiceProxy(isPremiumUser = false)

    println("Premium User Request:")
    println(premiumUserProxy.streamVideo("premium_video_123"))

    println("Regular User Request:")
    println(regularUserProxy.streamVideo("premium_video_123"))
}

Output

Kotlin
Premium User Request:
Streaming video content for video ID: premium_video_123

Regular User Request:
Upgrade to premium to stream this video.

Here,

  • Interface (VideoService): The VideoService interface defines the contract for streaming video.
  • Real Object (RealVideoService): The RealVideoService implements the VideoService interface, providing the actual video streaming functionality.
  • Proxy Class (VideoServiceProxy): The VideoServiceProxy class implements the VideoService interface and controls access to RealVideoService. It checks whether the user is premium and either allows streaming or restricts it.
  • Client: The client interacts with VideoServiceProxy, not with RealVideoService. The proxy makes the access control transparent for the client.

Benefits and Limitations

Benefits

  • Controlled Access: Allows us to restrict access based on custom logic.
  • Lazy Initialization: We can load resources only when required.
  • Security: Additional security checks can be implemented.

Limitations

  • Complexity: It can add unnecessary complexity if access control is not required.
  • Performance: May slightly impact performance because of the extra layer.

Conclusion

The Proxy design pattern is a powerful tool for managing access, adding control, and optimizing performance in your applications. By introducing a proxy, you can enforce access restrictions, add caching, or defer resource-intensive operations, all without changing the core functionality of the real object. In our video streaming example, the proxy ensures only authorized users can access premium content, demonstrating how this pattern can provide both flexibility and security. Mastering the Proxy pattern in Kotlin can help you build more robust and scalable applications, making it a valuable addition to your design pattern toolkit.

Facade Design Pattern

Master the Facade Design Pattern in Kotlin: Simplifying Complex Interactions

Ever notice how every app we use—from Amazon to our banking apps—makes everything seem so effortless? What we see on the surface is just the tip of the iceberg. Beneath that sleek interface lies a mountain of complex code working tirelessly to ensure everything runs smoothly. This is where the Facade Design Pattern shines, providing a way to hide all those intricate details and offering us a straightforward way to interact with complex systems.

So, what exactly is a facade? Think of it as a smooth layer that conceals the complicated stuff, allowing us to focus on what truly matters. In coding, this pattern lets us wrap multiple components or classes into one easy-to-use interface, making our interactions clean and simple. And if you’re using Kotlin, implementing this pattern is a breeze—Kotlin’s modern syntax and interfaces make creating facades feel effortless.

You might be wondering, “Isn’t this just like data hiding in OOP?” Not quite! Facades are more about simplifying access to complex systems rather than merely keeping details private. So, let’s dive in, explore what makes the Facade Pattern so powerful, look at real-life examples, and see the ups and downs of using it in Kotlin. Let’s get started!

Facade Design Pattern

The Facade pattern is part of the structural design patterns in the well-known Gang of Four (GoF) design patterns. This pattern provides a simplified interface to a complex subsystem, which may involve multiple classes and interactions. The primary goal of the Facade pattern is to reduce the complexity by creating a single entry point that manages complex logic behind the scenes, allowing the client (user of the code) to interact with a simplified interface.

In simple words, instead of directly interacting with multiple classes, methods, or modules within a complex subsystem, a client can work with a single Facade class that handles the complexities.

Imagine you’re trying to use a complex appliance with lots of buttons and settings. Instead of figuring out how to navigate all those features, you just have a single, easy-to-use control panel that manages everything behind the scenes. That’s exactly what the Facade pattern does.

It creates a straightforward interface that acts as a single entry point to a complex subsystem. This way, you don’t have to deal with multiple classes or methods directly; you can just interact with the Facade class, which takes care of all the complexity for you. It’s all about making things easier and less overwhelming!

I always believe that to truly use or understand any design pattern, it’s essential to grasp its structure first. Once we have a solid understanding of how it works, we can apply it to our everyday coding. So, let’s take a look at the structure of facade pattern first, and then we can dive into the coding part together.

Structure of the Facade Design Pattern

In the Facade Pattern, we have:

  1. Subsystem classes that handle specific, granular tasks.
  2. A Facade class that provides a simplified interface to these subsystems, delegating requests to the appropriate classes.

Let’s see how this looks in Kotlin.

Simple Scenario

Think about our office coffee maker for a second. When we want to brew our favorite blend, we often have to click multiple buttons on the control panel. Let’s see how we can make coffee with a single click using the Facade pattern in our code.

We’ll create a CoffeeMaker class that includes complex subsystems: a Grinder, a Boiler, and a CoffeeMachine. The CoffeeMakerFacade will provide a simple interface for the user to make coffee without dealing with the underlying complexity.

Kotlin
// Subsystem 1: Grinder
class Grinder {
    fun grindBeans() {
        println("Grinding coffee beans...")
    }
}

// Subsystem 2: Boiler
class Boiler {
    fun heatWater() {
        println("Heating water...")
    }
}

// Subsystem 3: CoffeeMachine
class CoffeeMachine {
    fun brewCoffee() {
        println("Brewing coffee...")
    }
}

// Facade: CoffeeMakerFacade
class CoffeeMakerFacade(
    private val grinder: Grinder,
    private val boiler: Boiler,
    private val coffeeMachine: CoffeeMachine
) {
    fun makeCoffee() {
        println("Starting the coffee-making process...")
        grinder.grindBeans()
        boiler.heatWater()
        coffeeMachine.brewCoffee()
        println("Coffee is ready!")
    }
}

// Client code
fun main() {
    // Creating subsystem objects
    val grinder = Grinder()
    val boiler = Boiler()
    val coffeeMachine = CoffeeMachine()

    // Creating the Facade
    val coffeeMaker = CoffeeMakerFacade(grinder, boiler, coffeeMachine)

    // Using the Facade to make coffee
    coffeeMaker.makeCoffee()
}



// Output 

Starting the coffee-making process...
Grinding coffee beans...
Heating water...
Brewing coffee...
Coffee is ready!

Here,

Subsystems

  • Grinder: Handles the coffee bean grinding.
  • Boiler: Manages the heating of water.
  • CoffeeMachine: Responsible for brewing the coffee.

Facade

  • CoffeeMakerFacade: Simplifies the coffee-making process by providing a single method makeCoffee(), which internally calls the necessary methods from the subsystems in the correct order.

Client Code

  • The main() function creates instances of the subsystems and the facade. It then calls makeCoffee(), demonstrating how the facade abstracts the complexity of the underlying systems.

This is just a simple example to help us understand how the Facade pattern works. Next, we’ll explore another real-world scenario that’s more complex, but we’ll keep it simple.

Facade Pattern in Travel Booking System

Let’s say we want to provide a simple way for users to book their entire travel package in one go without worrying about booking each service (flight, hotel, taxi) individually. 

Here’s how the Facade pattern can help!

We’ll create a TravelFacade to handle flight, hotel, and taxi bookings, making the experience seamless. Each booking service—flight, hotel, and taxi—will have its own class with separate logic, while TravelFacade provides a unified interface to book the entire package.

Before we write the facade interface, let’s start by defining each booking service.

Kotlin
//Note: It's better to define each service in a separate file.

// FlightBooking.kt
class FlightBooking {
    fun bookFlight(from: String, to: String): String {
        // Simulate flight booking logic
        return "Flight booked from $from to $to"
    }
}

// HotelBooking.kt
class HotelBooking {
    fun bookHotel(location: String, nights: Int): String {
        // Simulate hotel booking logic
        return "$nights-night stay booked in $location"
    }
}

// TaxiBooking.kt
class TaxiBooking {
    fun bookTaxi(pickupLocation: String, destination: String): String {
        // Simulate taxi booking logic
        return "Taxi booked from $pickupLocation to $destination"
    }
}

Now, the TravelFacade class will act as a single interface that the client interacts with to book their entire travel package.

Kotlin
// TravelFacade.kt
class TravelFacade {
    private val flightBooking = FlightBooking()
    private val hotelBooking = HotelBooking()
    private val taxiBooking = TaxiBooking()

    fun bookFullPackage(from: String, to: String, hotelLocation: String, nights: Int): List<String> {
        val flightConfirmation = flightBooking.bookFlight(from, to)
        val hotelConfirmation = hotelBooking.bookHotel(hotelLocation, nights)
        val taxiConfirmation = taxiBooking.bookTaxi("Airport", hotelLocation)

        return listOf(flightConfirmation, hotelConfirmation, taxiConfirmation)
    }
}

And now, the client can simply use the TravelFacade without worrying about managing individual bookings for flights, hotels, and taxis

Kotlin
// Main.kt
fun main() {
    val travelFacade = TravelFacade()
    
    val travelPackage = travelFacade.bookFullPackage(
        from = "New York",
        to = "Paris",
        hotelLocation = "Paris City Center",
        nights = 5
    )

    // Display the booking confirmations
    travelPackage.forEach { println(it) }
}

Output

Kotlin
Flight booked from Pune to Andaman and Nicobar Islands
5-night stay booked in Welcomhotel By ITC Hotels, Marine Hill, Port Blair
Taxi booked from Airport to Welcomhotel By ITC Hotels, Marine Hill, Port Blair

Here,

  • Individual services (FlightBooking, HotelBooking, TaxiBooking) have their own booking logic.
  • TravelFacade abstracts the booking process, allowing the client to book a complete package with one call to bookFullPackage().
  • The client doesn’t need to understand or interact with each subsystem directly.

Let’s look at another use case in Android. Facade can be applied across different architectures, but I’ll give a more general view so anyone can easily relate and apply it in their code.

Network Communication Facade in Android

Creating a Network Communication Facade in Android with Kotlin helps us streamline and simplify how we interact with different network APIs and methods. This pattern lets us hide the complex details of various network operations, providing the app with a single, easy-to-use interface for making network requests. It’s especially handy when you want to work with multiple networking libraries or APIs in a consistent way.

Here’s a look at how a Network Communication Facade could work in Kotlin

First, let’s start by creating a NetworkFacade interface.

This interface defines the available methods for network operations (we’ll keep it simple with common methods like GET and POST). Any network client can implement this interface to handle requests.

Kotlin
interface NetworkFacade {
    suspend fun get(url: String): Result<String>
    suspend fun post(url: String, body: Map<String, Any>): Result<String>
    // Additional HTTP methods can be added if needed
}

Now, let’s implement this interface with a network client, such as Retrofit or OkHttp. Here, I’ll use OkHttp as an example.

Kotlin
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Response
import org.json.JSONObject
import kotlin.Result

class OkHttpNetworkFacade : NetworkFacade {

    private val client = OkHttpClient()

    override suspend fun get(url: String): Result<String> {
        val request = Request.Builder()
            .url(url)
            .get()
            .build()

        return client.newCall(request).execute().use { response ->
            if (response.isSuccessful) {
                Result.success(response.body?.string() ?: "")
            } else {
                Result.failure(Exception("GET request failed with code: ${response.code}"))
            }
        }
    }

    override suspend fun post(url: String, body: Map<String, Any>): Result<String> {
        val json = JSONObject(body).toString()
        val requestBody = json.toRequestBody("application/json; charset=utf-8".toMediaType())

        val request = Request.Builder()
            .url(url)
            .post(requestBody)
            .build()

        return client.newCall(request).execute().use { response ->
            if (response.isSuccessful) {
                Result.success(response.body?.string() ?: "")
            } else {
                Result.failure(Exception("POST request failed with code: ${response.code}"))
            }
        }
    }
}

If we need to switch to Retrofit for other services, we can also implement the same interface for Retrofit.

Kotlin
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Url

interface RetrofitApiService {
    @GET
    suspend fun get(@Url url: String): String

    @POST
    suspend fun post(@Url url: String, @Body body: Map<String, Any>): String
}

class RetrofitNetworkFacade : NetworkFacade {
    
    private val api: RetrofitApiService = Retrofit.Builder()
        .baseUrl("https://use-your-base-url-here.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()
        .create(RetrofitApiService::class.java)

    override suspend fun get(url: String): Result<String> {
        return try {
            val response = api.get(url)
            Result.success(response)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    override suspend fun post(url: String, body: Map<String, Any>): Result<String> {
        return try {
            val response = api.post(url, body)
            Result.success(response)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

Now, we can use the NetworkFacade in the application without worrying about which implementation is in use. This makes it easy to switch between different networking libraries if needed.

Kotlin
class NetworkRepository(private val networkFacade: NetworkFacade) {

    suspend fun fetchData(url: String): Result<String> {
        return networkFacade.get(url)
    }

    suspend fun sendData(url: String, data: Map<String, Any>): Result<String> {
        return networkFacade.post(url, data)
    }
}

To enable flexible configuration, we can use dependency injection (DI) to inject the desired facade implementation—either OkHttpNetworkFacade or RetrofitNetworkFacade—when creating the NetworkRepository.

Kotlin
// Use OkHttpNetworkFacade
val networkRepository = NetworkRepository(OkHttpNetworkFacade())

// Or use RetrofitNetworkFacade
val networkRepository = NetworkRepository(RetrofitNetworkFacade())

Here,

NetworkFacade: This interface defines our network operations. Each client, whether it’s OkHttp or Retrofit, can implement this interface, offering different underlying functionalities while maintaining a consistent API for the application.

Result: We use a Result type to manage successful and failed network calls, which reduces the need for multiple try-catch blocks.

NetworkRepository: The repository interacts with the network clients through the facade. It doesn’t need to know which client is in use, providing flexibility and simplifying testing.

This structure allows us to add more network clients (like Ktor) in the future or easily swap out existing ones without changing the application logic that relies on network requests.

Benefits of the Facade Pattern

The Facade pattern offers several advantages, especially when dealing with complex systems. Here are a few key benefits:

  • Simplifies Usage: It hides the complexity of subsystems and provides a single point of access, making it easier for clients to interact with the system.
  • Improves Readability and Maintainability: With a unified interface, understanding the code flow becomes much simpler, which helps in maintaining the code over time.
  • Reduces Dependencies: It decouples clients from subsystems, allowing for changes in the underlying system without impacting the client code.
  • Increases Flexibility: Changes can be made within the subsystems without affecting the clients using the Facade, providing greater adaptability to future requirements.

When to Use the Facade Pattern

  • To Simplify Interactions: Use the Facade pattern when you need to simplify interactions with complex systems or subsystems.
  • To Hide Complexity: It’s ideal for hiding complexity from the client, making the system easier to use.
  • To Improve Code Readability: The Facade pattern helps enhance code readability by providing a clean, easy-to-understand interface.
  • To Maintain a Single Point of Entry: This pattern allows for a single point of entry to different parts of the codebase, which can help manage dependencies effectively.

Disadvantages of the Facade Pattern

While the Facade pattern offers many advantages, it’s essential to consider its drawbacks:

  • Potential Over-Simplification: By hiding the underlying complexity, the facade can limit access to the detailed functionality of the subsystem. If users need to interact with specific features not exposed through the facade, they might find it restrictive. For instance, consider a multimedia library with a facade for playing audio and video. If this facade doesn’t allow for adjustments to audio settings like bass or treble, users requiring those tweaks will have to dig into the subsystem, undermining the facade’s purpose.
  • Increased Complexity in the Facade: If the facade attempts to manage too many subsystem methods or functionalities, it can become complex itself. This contradicts the goal of simplicity and may require more maintenance. Imagine a facade for a comprehensive payment processing system that tries to include methods for credit card payments, digital wallets, and subscription management. If the facade becomes too feature-rich, it can turn into a large, unwieldy class, making it hard to understand or modify.
  • Encapsulation Leakage: The facade pattern can lead to situations where clients become aware of too many details about the subsystems, breaking encapsulation. This can complicate future changes to the subsystem, as clients might depend on specific implementations. For example, if a facade exposes the internal state of a subsystem (like the current status of a printer), clients might start using that state in their logic. If the internal implementation changes (like adopting a new status management system), it could break clients relying on the old state structure.
  • Not Always Necessary: For simpler systems, implementing a facade can add unnecessary layers. If the subsystem is already easy to use or doesn’t consist of many components, the facade may be redundant. For example, if you have a simple logging system with a few straightforward methods (like logInfo and logError), creating a facade to wrap these methods might be overkill. In such cases, direct access to the logging methods may be clearer and easier for developers.

Conclusion

The Facade Pattern is a great choice when you want to simplify complex interactions between multiple classes or subsystems. By creating a single entry point, you can make your code much easier to use and understand. With Kotlin’s class structure and concise syntax, implementing this pattern feels smooth and straightforward.

When used thoughtfully, the Facade Pattern can greatly improve code readability, maintainability, and overall usability—especially in complex projects like multimedia systems, payment gateways, or extensive frameworks. Just remember to balance its benefits with potential drawbacks to ensure it aligns with your design goals.

Happy coding! Enjoy creating clean and intuitive interfaces with the Facade Pattern!

Flyweight Design Pattern

Master the Flyweight Design Pattern in Kotlin: Effortlessly Optimize Memory and Boost Game Performance

Have you ever stopped to marvel at how breathtaking mobile games have become? Think about the background graphics in popular games like PUBG or Pokémon GO. (Honest confession: I haven’t played these myself, but back in my college days, my friends and I used to have epic Counter-Strike and I.G.I. 2 sessions—and, funny enough, the same group now plays PUBG—except me!) Anyway, the real question is: have you noticed just how detailed the game worlds are? You’ve got lush grass fields, towering trees, cozy houses, and fluffy clouds—so many objects that make these games feel alive. But here’s the kicker: how do they manage all that without your phone overheating or the game lagging as the action intensifies?

It turns out that one of the sneaky culprits behind game lag is the sheer number of objects being created over and over, all of which take up precious memory. That’s where the Flyweight Design Pattern swoops in like a hero.

Picture this: you’re playing a game with hundreds of trees, houses, and patches of grass. Now, instead of creating a brand-new tree or patch of grass every time, wouldn’t it make sense to reuse these elements when possible? After all, many of these objects share the same traits—like the green color of grass or the texture of tree leaves. The Flyweight pattern allows the game to do just that: reuse common properties across objects to save memory and keep performance snappy.

In this blog, we’re going to break down exactly how the Flyweight Design Pattern works, why it’s such a game-changer for handling large numbers of similar objects, and how you can use it to optimize your own projects. Let’s dive in and find out how it all works!

What is the Flyweight Pattern?

The Flyweight Pattern is a structural design pattern that reduces memory consumption by sharing as much data as possible with similar objects. Instead of storing the same data repeatedly across multiple instances, the Flyweight pattern stores shared data in a common object and only uses unique data in individual objects. This concept is particularly useful when creating a large number of similar objects.

The core idea behind this pattern is to:

1. Identify and separate intrinsic and extrinsic data.

        Intrinsic data is shared across all objects and remains constant.

        Extrinsic data is unique to each object instance.

      2. Store intrinsic data in a shared flyweight object, and pass extrinsic data at runtime.

      Let’s break it down with our game example. Imagine a field of grass where each blade has a common characteristic: color. All the grass blades are green, which remains constant across the entire field. This is the intrinsic data — something that doesn’t change, like the color.

      Now, think about the differences between the blades of grass. Some may vary in height or shape, like a blade being wider in the middle or taller than another. Additionally, the exact position of each blade in the field differs. These varying factors, such as height and position, are the extrinsic data.

      Without using the Flyweight pattern, you would store both the common and unique data for each blade of grass in separate objects, which quickly leads to redundancy and memory bloat. However, with the Flyweight pattern, we extract the common data (the color) and share it across all grass blades using one object. The varying data (height, shape, and position) is stored separately for each blade, reducing memory usage significantly.

      In short, the Flyweight pattern helps optimize your game by sharing common attributes while keeping only the unique properties in separate objects. This is especially useful when working with a large number of similar objects like blades of grass in a game.

      Structure of Flyweight Design Pattern

      Before we jump into the code, let’s break down the structure of the Flyweight Design Pattern to understand how it works:

      Flyweight

      • Defines an interface that allows flyweight objects to receive and act on external (extrinsic) state.

      ConcreteFlyweight

      • Implements the Flyweight interface and stores intrinsic state (internal, unchanging data).
      • Must be shareable across different contexts.

      UnsharedConcreteFlyweight

      • While the Flyweight pattern focuses on sharing information, there can be cases where instances of concrete flyweight classes are not shared. These objects may hold their own state.

      FlyweightFactory

      • Creates and manages flyweight objects.
      • Ensures that flyweight objects are properly shared to avoid duplication.

      Client

      • Holds references to flyweight objects.
      • Computes or stores the external (extrinsic) state that the flyweights use.

      Basically, the Flyweight pattern works by dividing the object state into two categories:

      1. Intrinsic State: Data that can be shared across multiple objects. It is stored in a shared, immutable object.
      2. Extrinsic State: Data that is unique to each object instance and is passed to the object when it is used.

      Using a Flyweight Factory, objects that share the same intrinsic state are created once and reused multiple times. This approach leads to significant memory savings.

      Real World Examples

      Grass Field Example

      Let’s first implement the Flyweight pattern with a grass field example by creating a Flyweight interface, defining concrete Flyweight classes, building a factory to manage them, and demonstrating their usage with a client.

      Define the Flyweight Interface

      This interface will declare the method that the flyweight objects will implement.

      Kotlin
      interface GrassBlade {
          fun display(extrinsicState: GrassBladeState)
      }
      
      Create ConcreteFlyweight Class

      This class represents the shared intrinsic state (color) of the grass blades.

      Kotlin
      class ConcreteGrassBlade(private val color: String) : GrassBlade {
          override fun display(extrinsicState: GrassBladeState) {
              println("Grass Blade Color: $color, Height: ${extrinsicState.height}, Position: ${extrinsicState.position}")
          }
      }
      
      Create UnsharedConcreteFlyweight Class (if necessary)

      If you need blades that may have unique characteristics, you can have this class. For simplicity, we won’t implement any specific logic here.

      Kotlin
      class UnsharedConcreteGrassBlade : GrassBlade {
          // Implementation for unshared concrete flyweight if needed
          override fun display(extrinsicState: GrassBladeState) {
              // Not shared, just a placeholder
          }
      }
      
      Create the FlyweightFactory

      This factory class will manage the creation and sharing of grass blade objects.

      Kotlin
      class GrassBladeFactory {
          private val grassBlades = mutableMapOf<String, GrassBlade>()
      
          fun getGrassBlade(color: String): GrassBlade {
              return grassBlades.computeIfAbsent(color) { ConcreteGrassBlade(color) }
          }
      }
      
      Define the Extrinsic State

      This class holds the varying data for each grass blade.

      Kotlin
      data class GrassBladeState(val height: Int, val position: String)
      Implement the Client Code

      Here, we will create instances of grass blades and display their properties using the Flyweight pattern.

      Kotlin
      fun main() {
          val grassBladeFactory = GrassBladeFactory()
      
          // Creating some grass blades with shared intrinsic state (color) and varying extrinsic state
          val grassBlades = listOf(
              Pair(grassBladeFactory.getGrassBlade("Green"), GrassBladeState(5, "A1")),
              Pair(grassBladeFactory.getGrassBlade("Green"), GrassBladeState(7, "A2")),
              Pair(grassBladeFactory.getGrassBlade("Green"), GrassBladeState(6, "B1")),
              Pair(grassBladeFactory.getGrassBlade("Green"), GrassBladeState(4, "B2")),
              Pair(grassBladeFactory.getGrassBlade("Green"), GrassBladeState(5, "C1")),
              Pair(grassBladeFactory.getGrassBlade("Green"), GrassBladeState(5, "C2"))
          )
      
          // Displaying the grass blades
          for ((grassBlade, state) in grassBlades) {
              grassBlade.display(state)
          }
      }

      Output

      Kotlin
      Grass Blade Color: Green, Height: 5, Position: A1
      Grass Blade Color: Green, Height: 7, Position: A2
      Grass Blade Color: Green, Height: 6, Position: B1
      Grass Blade Color: Green, Height: 4, Position: B2
      Grass Blade Color: Green, Height: 5, Position: C1
      Grass Blade Color: Green, Height: 5, Position: C2

      Here,

      • GrassBlade Interface: This defines a method display that takes an extrinsicState.
      • ConcreteGrassBlade Class: Implements the Flyweight interface and stores the intrinsic state (color).
      • GrassBladeFactory: Manages the creation and sharing of ConcreteGrassBlade instances based on color.
      • GrassBladeState Class: Holds the extrinsic state for each grass blade, such as height and position.
      • Main Function: This simulates the game, creates multiple grass blades, and demonstrates how shared data and unique data are handled efficiently.

      Forest Example

      Let’s build the forest now. Here, we’ll render a forest with grass, trees, and flowers, reusing objects to minimize memory usage and improve performance by applying the Flyweight pattern.

      In this example, the ForestObject will represent a shared object (like grass, trees, and flowers), and the Forest class will be responsible for managing and rendering these objects with variations in their positions and other properties.

      Kotlin
      // Step 1: Flyweight Interface
      interface ForestObject {
          fun render(x: Int, y: Int) // Render object at given coordinates
      }
      
      // Step 2: Concrete Flyweight Classes (Grass, Tree, Flower)
      class Grass : ForestObject {
          private val color = "Green" // Shared property
      
          override fun render(x: Int, y: Int) {
              println("Rendering Grass at position ($x, $y) with color $color")
          }
      }
      
      class Tree : ForestObject {
          private val type = "Oak" // Shared property
      
          override fun render(x: Int, y: Int) {
              println("Rendering Tree of type $type at position ($x, $y)")
          }
      }
      
      class Flower : ForestObject {
          private val color = "Yellow" // Shared property
      
          override fun render(x: Int, y: Int) {
              println("Rendering Flower at position ($x, $y) with color $color")
          }
      }
      
      // Step 3: Flyweight Factory (Manages the creation and reuse of objects)
      class ForestObjectFactory {
          private val objects = mutableMapOf<String, ForestObject>()
      
          // Returns an existing object or creates a new one if it doesn't exist
          fun getObject(type: String): ForestObject {
              return objects.getOrPut(type) {
                  when (type) {
                      "Grass" -> Grass()
                      "Tree" -> Tree()
                      "Flower" -> Flower()
                      else -> throw IllegalArgumentException("Unknown forest object type")
                  }
              }
          }
      }
      
      // Step 4: Forest Class (Client code to manage and render the forest)
      class Forest(private val factory: ForestObjectFactory) {
          private val objectsInForest = mutableListOf<Pair<ForestObject, Pair<Int, Int>>>()
      
          // Adds a new object with specified type and coordinates
          fun plantObject(type: String, x: Int, y: Int) {
              val forestObject = factory.getObject(type)
              objectsInForest.add(Pair(forestObject, Pair(x, y)))
          }
      
          // Renders the entire forest
          fun renderForest() {
              for ((obj, position) in objectsInForest) {
                  obj.render(position.first, position.second)
              }
          }
      }
      
      // Step 5: Testing the Flyweight Pattern
      fun main() {
          val factory = ForestObjectFactory()
          val forest = Forest(factory)
      
          // Planting various objects in the forest (reusing the same objects)
          forest.plantObject("Grass", 10, 20)
          forest.plantObject("Grass", 15, 25)
          forest.plantObject("Tree", 30, 40)
          forest.plantObject("Tree", 35, 45)
          forest.plantObject("Flower", 50, 60)
          forest.plantObject("Flower", 55, 65)
      
          // Rendering the forest
          forest.renderForest()
      }
      

      Here,

      Flyweight Interface (ForestObject): This interface defines the method render, which will be used to render objects in the forest.

      Concrete Flyweights (Grass, Tree, Flower): These classes implement the ForestObject interface and have shared properties like color and type that are common across multiple instances.

      Flyweight Factory (ForestObjectFactory): This class manages the creation and reuse of objects. It ensures that if an object of the same type already exists, it will return the existing object rather than creating a new one.

      Client Class (Forest): This class is responsible for planting objects in the forest and rendering them. It uses the factory to obtain objects and stores their positions.

      Main Function: In the main function, we plant several objects in the forest, but thanks to the Flyweight Design Pattern, we reuse existing objects to save memory.

      Output

      Kotlin
      Rendering Grass at position (10, 20) with color Green
      Rendering Grass at position (15, 25) with color Green
      Rendering Tree of type Oak at position (30, 40)
      Rendering Tree of type Oak at position (35, 45)
      Rendering Flower at position (50, 60) with color Yellow
      Rendering Flower at position (55, 65) with color Yellow

      Basically, even though we planted multiple grass, tree, and flower objects, the game only created one object per type, thanks to the factory. These objects are reused, with only their positions varying. This approach saves memory and improves performance, especially when there are thousands of similar objects in the game world.

      Few more Usecases 

      1. Text Rendering Systems: Letters that share the same font, size, and style can be stored as flyweights, while the position of each character in the document is extrinsic.
      2. Icons in Operating Systems: Many icons in file browsers share the same image but differ in position or name.
      3. Web Browsers: Rendering engines often use flyweights to manage CSS style rules, ensuring that the same styles aren’t recalculated multiple times.

      Advantages of the Flyweight Pattern

      • Memory Efficient: Reduces the number of objects by sharing common data, thus saving memory.
      • Improves Performance: With fewer objects to manage, the program can run faster.
      • Scalability: Useful in applications with many similar objects (e.g., games, graphical applications).

      Drawbacks of the Flyweight Pattern

      • Increased Complexity: The pattern introduces more complexity as you need to manage both intrinsic and extrinsic state separately.
      • Less Flexibility: Changes to the intrinsic state can affect all instances of the flyweight, which might not be desired in all situations.
      • Thread-Safety Issues: Careful management of shared state is required in a multi-threaded environment.

      When to Use Flyweight Pattern

      1. When an application has many similar objects: If creating each object individually would use too much memory, like in games with numerous characters or environments.
      2. When object creation is expensive: Reusing objects can prevent the overhead of frequently creating new objects.
      3. When intrinsic and extrinsic states can be separated: The pattern is effective when most object properties are shareable.

      Conclusion

      The Flyweight design pattern is a powerful tool when you need to optimize memory usage by sharing objects with similar properties. In this post, we explored how the Flyweight pattern works, saw a real-world analogy, and implemented it in Kotlin using grass field and forest examples in a game.

      While the Flyweight pattern can be a great way to reduce memory usage, it’s important to carefully analyze whether it’s necessary in your specific application. For simple applications, this pattern might introduce unnecessary complexity. However, when dealing with a large number of objects with similar characteristics, Flyweight is a great choice.

      By understanding the intrinsic and extrinsic state separation, you can effectively implement the Flyweight pattern in Kotlin to build more efficient applications.

      Decorator Design Pattern

      Decorator Design Pattern: Unleash the Power of Dynamic Behavior Enhancement in Kotlin

      The Decorator Design Pattern is a powerful structural design pattern that lets you enhance the behavior of an object on the fly, without touching the code of other objects from the same class. It’s like giving your object a superpower without changing its DNA! This approach offers a smarter alternative to subclassing, allowing you to extend functionality in a flexible and dynamic way.

      In this blog, we’ll take a deep dive into the Decorator Design Pattern in Kotlin, uncovering its use cases and walking through practical examples. We’ll start with the basic concept and then dive into code examples to make everything crystal clear. Let’s get started!

      What is the Decorator Design Pattern?

      The Decorator Pattern allows you to dynamically add behavior to an object without modifying its original structure. It works by wrapping an object with another object that provides additional functionality. This pattern is highly effective when extending the behavior of classes, avoiding the complexity of subclassing. 

      Think of this pattern as an alternative to subclassing. Instead of creating a large hierarchy of subclasses to add functionality, we create decorator classes that add functionality by wrapping the base object.

      Imagine you have a simple object, like a plain cake. If you want to add chocolate or sprinkles to the cake, you don’t have to create new cakes like ChocolateCake or SprinkleCake. Instead, you wrap the plain cake with decorators like ChocolateDecorator or SprinkleDecorator, adding the extra features.

      Before diving into the code, let’s first look at the basic structure of the Decorator design pattern. This will give us better clarity as we move forward and tackle more problems with the code.

      Basic Components of the Decorator Design Pattern

      Decorator Design Pattern Structure
      • Component: The interface or abstract class defining the structure for objects that can have responsibilities added to them dynamically.
      • Concrete Component: The class that is being decorated.
      • Decorator: Abstract class or interface that wraps the component and provides additional functionality.
      • Concrete Decorator: The specific implementation of the decorator class that adds new behaviors.

      I know many of us might not see the connection, so let’s explore how this works together.

      Let’s use our cake example,

      1. Component (Base Interface or Abstract Class): This is the original object you want to add features to. In our case, it’s a “Cake.”
      2. ConcreteComponent: This is the base class that implements the component. This is the plain cake.
      3. Decorator (Abstract Class or Interface): This class is the wrapper that contains a reference to the component and can add new behavior.
      4. ConcreteDecorator: This is a specific decorator that adds new behavior, like adding chocolate or sprinkles to the cake.

      Now, let’s demonstrate this in Kotlin using a simple code snippet.

      Step 1: Define the Component Interface

      The component defines the base functionality. In our case, we will call it Cake.

      Kotlin
      // Component
      interface Cake {
          fun bake(): String
      }
      

      Step 2: Create a Concrete Component (The Plain Cake)

      This is the “base” version of the object, which we can decorate later. It has the basic functionality.

      Kotlin
      // Concrete Component
      class PlainCake : Cake {
          override fun bake(): String {
              return "Plain Cake"
          }
      }
      

      Step 3: Create the Decorator Class

      The decorator class implements the Cake interface and holds a reference to a Cake object (the one being decorated).

      Kotlin
      // Decorator
      abstract class CakeDecorator(private val cake: Cake) : Cake {
          override fun bake(): String {
              return cake.bake() // Delegating the call to the wrapped object
          }
      }

      Step 4: Create Concrete Decorators (Add Chocolate and Sprinkles)

      These are specific decorators that add new behavior. For example, adding chocolate or sprinkles to the cake.

      Kotlin
      // Concrete Decorator 1: Adding Chocolate
      class ChocolateDecorator(cake: Cake) : CakeDecorator(cake) {
          override fun bake(): String {
              return super.bake() + " with Chocolate"
          }
      }
      
      // Concrete Decorator 2: Adding Sprinkles
      class SprinkleDecorator(cake: Cake) : CakeDecorator(cake) {
          override fun bake(): String {
              return super.bake() + " with Sprinkles"
          }
      }
      

      Step 5: Use the Decorators

      Now you can take a plain cake and add different decorators (chocolate and sprinkles) to it dynamically.

      Kotlin
      fun main() {
          // Create a plain cake
          val plainCake = PlainCake()
      
          // Decorate the plain cake with chocolate
          val chocolateCake = ChocolateDecorator(plainCake)
          println(chocolateCake.bake()) // Output: Plain Cake with Chocolate
      
          // Further decorate the cake with sprinkles
          val sprinkleChocolateCake = SprinkleDecorator(chocolateCake)
          println(sprinkleChocolateCake.bake()) // Output: Plain Cake with Chocolate with Sprinkles
      }
      

      Here, PlainCake is our base object, while ChocolateDecorator and SprinkleDecorator are the wrappers that add delightful flavors without altering the original PlainCake class. You can mix and match these decorators any way you like, dynamically enhancing the cake without changing its original essence.

      But wait, here’s a thought! 

      You might wonder: since we’re using both inheritance and composition here, why not rely solely on inheritance? Why do we need the help of composition?

      And here’s another interesting point: have you noticed how we can avoid the hassle of creating countless subclasses for every combination of behaviors, like ChocolateCake, SprinkleCake, and ChocolateSprinkleCake? Instead, we can simply ‘decorate’ an object with as many behaviors as we want, dynamically, at runtime!

      Alright, let’s play a little guessing game… 🤔 Ah, yes! No — wait 😕, it’s actually a no! Now that we’ve had our fun, let’s dive deeper into the problem the Decorator Pattern solves: how it helps us avoid subclass explosion while still offering dynamic behavior at runtime.

      I’ll walk you through a real-life scenario to illustrate this before we jump into the code. Let’s break it down into two key points:

      1. Inheritance vs. Composition in the Decorator Pattern
      2. How this combination avoids subclass explosion while enabling dynamic behavior.

      Inheritance vs. Composition in the Decorator Pattern

      In the Decorator Pattern, we indeed use both inheritance and composition together. Here’s how:

      • Inheritance: Decorators and the base class share a common interface. This is the type system‘s way to ensure that both the decorated object and the original object can be used in the same way (i.e., they both implement the same methods). This is why we inherit from a common interface or abstract class.
      • Composition: Instead of adding behavior via inheritance (which creates subclass explosion), we use composition to wrap objects. Each decorator contains an instance of the object it’s decorating. This wrapping allows us to combine behaviors in different ways at runtime.

      By using composition (wrapping objects) instead of inheritance (creating subclasses for every combination), the Decorator Pattern allows us to avoid the explosion of subclasses.

      Let’s compare this with inheritance-only and then with the Decorator Pattern.

      How the Decorator Pattern Avoids Subclass Explosion

      Subclass Explosion Problem (Inheritance-Only Approach)

      Imagine we have a simple notification system where we want to add sound, vibration, and banner features to notifications. Using inheritance alone, we might end up with:

      Kotlin
      // Base notification
      open class Notification {
          open fun send() = "Sending Notification"
      }
      
      // Subclass 1: Add Sound
      class SoundNotification : Notification() {
          override fun send() = super.send() + " with Sound"
      }
      
      // Subclass 2: Add Vibration
      class VibrationNotification : Notification() {
          override fun send() = super.send() + " with Vibration"
      }
      
      // Subclass 3: Add Banner
      class BannerNotification : Notification() {
          override fun send() = super.send() + " with Banner"
      }
      
      // Now we need to combine all features
      class SoundVibrationNotification : Notification() {
          override fun send() = super.send() + " with Sound and Vibration"
      }
      
      class SoundBannerNotification : Notification() {
          override fun send() = super.send() + " with Sound and Banner"
      }
      
      class VibrationBannerNotification : Notification() {
          override fun send() = super.send() + " with Vibration and Banner"
      }
      
      // And so on...
      

      Here, we need to create a new subclass for every combination:

      • SoundNotification
      • VibrationNotification
      • BannerNotification
      • SoundVibrationNotification
      • SoundBannerNotification
      • VibrationBannerNotification
      • …and so on!

      For three features, you end up with a lot of classes. This doesn’t scale well because for n features, you might need 2^n subclasses (combinations of features). This is called subclass explosion.

      How Decorator Pattern Solves This (Using Inheritance + Composition)

      With the Decorator Pattern, we use composition to dynamically wrap objects instead of relying on subclassing to mix behaviors.

      Here’s the key difference:

      • Inheritance is used only to ensure that both the base class (Notification) and the decorators (SoundNotificationDecorator, VibrationNotificationDecorator, etc.) implement the same interface.
      • Composition is used to “wrap” objects with additional behavior dynamically, at runtime.

      Let’s see how this works.

      Decorator Pattern Rocks

      First, we define the common interface (Notification) and the decorators:

      Kotlin
      // Step 1: Define the common interface (or abstract class)
      interface Notification {
          fun send(): String
      }
      
      // Step 2: Implement the base notification class
      class BasicNotification : Notification {
          override fun send() = "Sending Basic Notification"
      }
      
      // Step 3: Create the abstract decorator class, inheriting from Notification
      abstract class NotificationDecorator(private val decoratedNotification: Notification) : Notification {
          override fun send(): String {
              return decoratedNotification.send() // Delegate to the wrapped object
          }
      }
      
      // Step 4: Implement concrete decorators
      class SoundNotificationDecorator(notification: Notification) : NotificationDecorator(notification) {
          override fun send(): String {
              return super.send() + " with Sound"
          }
      }
      
      class VibrationNotificationDecorator(notification: Notification) : NotificationDecorator(notification) {
          override fun send(): String {
              return super.send() + " with Vibration"
          }
      }
      
      class BannerNotificationDecorator(notification: Notification) : NotificationDecorator(notification) {
          override fun send(): String {
              return super.send() + " with Banner"
          }
      }
      

      Here,

      • Common Interface (Notification): Both the base class (BasicNotification) and the decorators (SoundNotificationDecorator, VibrationNotificationDecorator, etc.) implement the Notification interface. This is where we use inheritance.
      • Composition: Instead of subclassing, each decorator contains another Notification object (which could be the base or another decorator) and wraps it with additional functionality.

      Dynamic Behavior at Runtime (No Subclass Explosion)

      Now, we can apply these decorators dynamically, without creating new subclasses for each combination:

      Kotlin
      fun main() {
          // Create a basic notification
          var notification: Notification = BasicNotification()
      
          // Dynamically add features at runtime using decorators
          notification = SoundNotificationDecorator(notification)
          notification = VibrationNotificationDecorator(notification)
          notification = BannerNotificationDecorator(notification)
      
          // Final notification with all features
          println(notification.send()) // Output: Sending Basic Notification with Sound with Vibration with Banner
      }
      

      Avoiding Subclass Explosion:

      • Instead of creating a class for each combination (like SoundVibrationBannerNotification), we combine behaviors dynamically by wrapping objects.
      • Using composition, we can mix and match behaviors as needed, avoiding the explosion of subclasses.

      Dynamic Behavior:

      • You can dynamically add or remove features at runtime by wrapping objects with decorators. For example, you can add sound, vibration, or banner as needed.
      • This gives you flexibility because you don’t have to predefine all possible combinations in the class hierarchy.

      Why Use Composition and Inheritance Together?

      • Inheritance ensures that the decorators and the original object can be used interchangeably since they all implement the same interface (Notification).
      • Composition lets us dynamically combine behaviors by wrapping objects instead of creating a new subclass for every possible feature combination.

      In short, the Decorator Pattern uses inheritance to define a common interface and composition to avoid subclass explosion by dynamically adding behaviors. This combination provides the flexibility to enhance object behavior at runtime without the need for a rigid subclass hierarchy.

      Real-Life Example — Enhancing a Banking Payment System with the Decorator Pattern

      Imagine you’re developing a banking payment system that starts off simple — just basic payment processing for transactions. But as the bank expands its services, you need to introduce extra features, like transaction fees or fraud detection, while keeping the core payment logic intact. How do you manage this without creating a tangled mess? That’s where the Decorator Pattern comes in. Let’s break it down step by step, adding these new banking features while maintaining a clean and flexible architecture.

      Note: This is just a simple example, but have you noticed similar trends with apps like GPay? When you recharge your mobile, you might encounter an extra platform fee. The same is true for apps like PhonePe, Flipkart, Swiggy, and more recently, Zomato, which raised platform fees during festive seasons like Diwali, where these fees have become increasingly common. Initially, these services offered simple, fee-free features. However, as the platforms evolved and expanded their offerings, additional layers — such as service fees and other enhancements — were introduced to support new functionalities. We don’t know exactly which approach they followed, but the Decorator Pattern would be a great fit for such use cases, as it allows for these additions without disrupting the core functionality.

      Let’s design this system step by step using the Decorator Pattern.

      Step 1: Defining the Component Interface

      We will start by defining a simple PaymentProcessor interface. This interface will have a method processPayment() that handles the basic payment process.

      Kotlin
      interface PaymentProcessor {
          fun processPayment(amount: Double)
      }
      

      Step 2: Implementing the Concrete Component

      The BasicPaymentProcessor class will be the concrete implementation of the PaymentProcessor interface. This class will simply process the payment without any additional behavior like fees or fraud checks.

      Kotlin
      class BasicPaymentProcessor : PaymentProcessor {
          override fun processPayment(amount: Double) {
              println("Processing payment of ₹$amount")
          }
      }
      

      This class represents the core logic for processing payments.

      Step 3: Creating the Decorator Class

      Now, we need to create an abstract class PaymentProcessorDecorator that will implement the PaymentProcessor interface and forward requests to the decorated object. This will allow us to add new behavior in subclasses.

      Kotlin
      abstract class PaymentProcessorDecorator(private val processor: PaymentProcessor) : PaymentProcessor {
          override fun processPayment(amount: Double) {
              processor.processPayment(amount)  // Forwarding the call to the wrapped component
          }
      }

      The PaymentProcessorDecorator acts as a wrapper for the original PaymentProcessor and can add extra functionality in the subclasses.

      Step 4: Implementing the Concrete Decorators

      Let’s now add two decorators:

      1. TransactionFeeDecorator: This adds a fee to the payment.
      2. FraudDetectionDecorator: This performs a fraud check before processing the payment.
      Transaction Fee Decorator

      This decorator adds a transaction fee on top of the payment amount.

      Kotlin
      class TransactionFeeDecorator(processor: PaymentProcessor) : PaymentProcessorDecorator(processor) {
          private val feePercentage = 2.5  // Let's assume a 2.5% fee on every transaction
      
          override fun processPayment(amount: Double) {
              val fee = amount * feePercentage / 100
              println("Applying transaction fee of ₹$fee")
              super.processPayment(amount + fee)  // Passing modified amount to the wrapped processor
          }
      }
      
      Fraud Detection Decorator

      This decorator performs a simple fraud check before processing the payment.

      Kotlin
      class FraudDetectionDecorator(processor: PaymentProcessor) : PaymentProcessorDecorator(processor) {
          override fun processPayment(amount: Double) {
              if (isFraudulentTransaction(amount)) {
                  println("Payment flagged as fraudulent! Transaction declined.")
              } else {
                  println("Fraud check passed.")
                  super.processPayment(amount)  // Proceed if fraud check passes
              }
          }
      
          private fun isFraudulentTransaction(amount: Double): Boolean {
              // Simple fraud detection logic: consider transactions above ₹10,000 as fraudulent for this example
              return amount > 10000
          }
      }
      

      Step 5: Using the Decorators

      Now that we have both decorators ready, let’s use them. We’ll create a BasicPaymentProcessor and then decorate it with both TransactionFeeDecorator and FraudDetectionDecorator to show how these can be combined.

      Kotlin
      fun main() {
          val basicProcessor = BasicPaymentProcessor()
      
          // Decorate the processor with transaction fees and fraud detection
          val processorWithFees = TransactionFeeDecorator(basicProcessor)
          val processorWithFraudCheckAndFees = FraudDetectionDecorator(processorWithFees)
      
          // Test with a small payment
          println("Payment 1:")
          processorWithFraudCheckAndFees.processPayment(5000.0)
      
          // Test with a large (fraudulent) payment
          println("\nPayment 2:")
          processorWithFraudCheckAndFees.processPayment(20000.0)
      }
      

      Output

      Kotlin
      Payment 1:
      Fraud check passed.
      Applying transaction fee of ₹125.0
      Processing payment of ₹5125.0
      
      Payment 2:
      Payment flagged as fraudulent! Transaction declined.
      

      In this case,

      1. Basic Payment Processing: We start with the BasicPaymentProcessor, which simply processes the payment.
      2. Adding Transaction Fees: The TransactionFeeDecorator adds a fee on top of the amount and forwards the modified amount to the BasicPaymentProcessor.
      3. Fraud Detection: The FraudDetectionDecorator checks if the transaction is fraudulent before forwarding the payment to the next decorator (or processor). If the transaction is fraudulent, it stops the process.

      By using the Decorator Pattern, we can flexibly add more behaviors like logging, authentication, or currency conversion without modifying the original PaymentProcessor class. This avoids violating the Open-Closed Principle (OCP), where classes should be open for extension but closed for modification.

      Why Use the Decorator Pattern?

      1. Flexibility: The Decorator Pattern provides more flexibility than inheritance. Instead of creating many subclasses for every combination of features, we use a combination of decorators.
      2. Open/Closed Principle: The core component class (like Cake, Notification and PaymentProcessor) remains unchanged. We can add new features (decorators) without altering existing code, making the system open for extension but closed for modification.
      3. Single Responsibility: Each decorator has a single responsibility: to add specific behavior to the object it wraps.

      When to Use the Decorator Pattern?

      • When you want to add behavior to objects dynamically.
      • When subclassing leads to too many classes and complicated hierarchies.
      • When you want to follow the Open/Closed principle and extend an object’s functionality without modifying its original class.

      Limitations of the Decorator Pattern

      While the Decorator Pattern is quite powerful, it has its limitations:

      • Increased Complexity: As the number of decorators increases, the system can become more complex to manage and understand, especially with multiple layers of decorators wrapping each other.
      • Debugging Difficulty: With multiple decorators, it can be harder to trace the flow of execution during debugging.

      Conclusion

      The Decorator Design Pattern offers a versatile and dynamic approach to enhancing object behavior without the need for extensive subclassing. By allowing you to “wrap” objects with additional functionality, it promotes cleaner, more maintainable code and encourages reusability. Throughout this exploration in Kotlin, we’ve seen how this pattern can be applied to real-world scenarios, making it easier to adapt and extend our applications as requirements evolve. Whether you’re adding features to a simple object or constructing complex systems, the Decorator Pattern provides a powerful tool in your design toolkit. Embrace the flexibility it offers, and you’ll find that your code can be both elegant and robust!

      error: Content is protected !!