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

Table of Contents

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

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

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

Command Design Pattern

At its core, the Command Design Pattern decouples the sender (the one making a request) from the receiver (the one handling the request). Instead of calling methods directly, the sender issues a command that encapsulates the details of the request. This way, the sender only knows about the command interface and not the specific implementation.

In short,

  • Sender: Issues commands.
  • Command: Encapsulates the request.
  • Receiver: Executes the request.

Structure of the Command Pattern

Before we dive into code, let’s see the primary components of this pattern:

  1. Command: An interface or abstract class defining a single method, execute().
  2. ConcreteCommand: Implements the Command interface and encapsulates the actions to be performed.
  3. Receiver: The object that performs the actual work.
  4. Invoker: The object that triggers the command’s execution.
  5. Client: The entity that creates and configures commands.

Command Pattern Implementation

Imagine a smart home system, similar to Google Home, where you can control devices like turning lights on/off or playing music. This scenario can be a great example to demonstrate the implementation of the Command design pattern.

Kotlin
// Command.kt
interface Command {
    fun execute()
}

Create Receivers

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

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

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

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

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

Create Concrete Commands

Each concrete command encapsulates a request to the receiver.

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

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

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

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

Create the Invoker

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

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

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

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

Client Code

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

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

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

    // Invoker
    val remoteControl = RemoteControl()

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

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

Here,

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

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

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

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

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

Enhancing the Pattern

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

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

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

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

Advantages of the Command Pattern

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

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

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

Command Queues: Commands can be queued for delayed execution.

Disadvantages

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

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

Conclusion

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

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

Happy coding..! 🚀

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!