Design Patterns - [3] Behavioral

Continue our software design patterns overview and a closer look at the Behavioral pattern.

2023-02-02 11:00:38 - Mohamad Abuzaid


In the previous article [2] Structural Design Patterns, we discussed in details the some of the most famous example of the Structural Design Patterns. Now we will continue with our next design pattern.

---------------------

[3] Behavioral Design Patterns


Behavioral design patterns are design patterns that focus on communication between objects, defining a way for objects to interact and collaborate with each other to accomplish a common goal. These patterns are concerned with the flow of control between objects and how they interact to produce a desired outcome.

[1] Chain of Responsibility, Command, Mediator, and Observer, address various ways of connecting senders and receivers of requests:

[2] Strategy lets the class vary its behavior when its strategy gets changed at runtime.

[3] Template Method defines the skeleton of an algorithm as an abstract class, allowing its subclasses to provide concrete behavior.

[4] Visitor lets you add new operations to classes of different objects without altering their source code.

Each of these patterns provides a unique solution to common problems that arise when designing software. Understanding the different behavioral design patterns can help you choose the best approach to solving complex problems and help you improve the overall design of your code.

----------------------------

(A) Chain of Responsibility Pattern


The Chain of Responsibility pattern is a design pattern used to handle requests or to forward requests to the next handler in a chain of handlers. The pattern provides a loosely coupled way of sending requests to one or more objects, while decoupling the sender and receiver of a request.

Here is an example implementation in Kotlin:

abstract class Handler {
    var next: Handler? = null

    abstract fun handleRequest(request: String)
}

class ConcreteHandler1(private val successor: Handler?) : Handler() {
    override fun handleRequest(request: String) {
        if (request == "Request1") {
            println("ConcreteHandler1 handled the request")
        } else {
            println("ConcreteHandler1 forwarding the request to the next handler")
            successor?.handleRequest(request)
        }
    }
}

class ConcreteHandler2(private val successor: Handler?) : Handler() {
    override fun handleRequest(request: String) {
        if (request == "Request2") {
            println("ConcreteHandler2 handled the request")
        } else {
            println("ConcreteHandler2 forwarding the request to the next handler")
            successor?.handleRequest(request)
        }
    }
}

fun main() {
    val handler1 = ConcreteHandler1(null)
    val handler2 = ConcreteHandler2(handler1)

    handler2.handleRequest("Request1")
    handler2.handleRequest("Request2")
    handler2.handleRequest("Request3")
}


In the example, ConcreteHandler1 and ConcreteHandler2 are the concrete handlers in the chain. When a request is received, each handler checks if it can handle the request. If it can, it handles it and returns. If it cannot, it forwards the request to the next handler in the chain. The chain ends when the last handler in the chain cannot handle the request and returns.

The Chain of Responsibility pattern is useful when you want to decouple the sender and receiver of a request, and when you want to allow multiple objects to handle a request. The pattern also makes it easy to add or remove handlers from the chain, without affecting the other handlers in the chain.


* Disadvantages:

The disadvantages of the Chain of Responsibility pattern are:

  1. Increased complexity: The Chain of Responsibility pattern can make the system more complex if there are too many handlers in the chain. It can also make the system more difficult to understand and maintain.
  2. Reduced performance: The Chain of Responsibility pattern can reduce performance if the chain is long, as each handler must check if it can handle the request before forwarding it to the next handler.
  3. Reduced flexibility: The Chain of Responsibility pattern can reduce flexibility if the handlers are hard-coded, as it can be difficult to change the handlers in the chain or to add new handlers.
  4. Reduced control: The Chain of Responsibility pattern can reduce control if it is not clear which handler is responsible for handling a particular request. This can result in errors or unexpected results.
  5. Reduced visibility: The Chain of Responsibility pattern can reduce visibility if it is not clear which handler is handling a particular request, as it can be difficult to determine where an error occurred.

Despite these disadvantages, the Chain of Responsibility pattern is still a useful design pattern in certain situations where decoupled communication is required between objects and multiple handlers can handle the same request.

----------------------------

(B) Command Pattern


The Command pattern is a behavioral design pattern that allows objects to encapsulate requests as objects and pass these requests to different receivers. This pattern provides a way to decouple the senders of requests from their receivers, allowing the senders to be ignorant of the receivers and the receivers to be ignorant of the senders.

Here’s a simple example of the Command pattern in Kotlin:

interface Command {
    fun execute()
}

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

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

class Switch {
    private val onCommands = mutableListOf<Command>()
    private val offCommands = mutableListOf<Command>()
    private var currentCommand: Command? = null

    fun setCommand(onCommand: Command, offCommand: Command) {
        onCommands.add(onCommand)
        offCommands.add(offCommand)
    }

    fun onButtonWasPushed(index: Int) {
        currentCommand = onCommands[index]
        currentCommand?.execute()
    }

    fun offButtonWasPushed(index: Int) {
        currentCommand = offCommands[index]
        currentCommand?.execute()
    }
}

class Light {
    fun turnOn() {
        println("Light is On")
    }

    fun turnOff() {
        println("Light is Off")
    }
}


In this example, the LightOnCommand and LightOffCommand classes implement the Command interface and encapsulate the request to turn on and off the light, respectively. The Switch class uses a list of Command objects to keep track of the on and off commands. When the onButtonWasPushed or offButtonWasPushed methods are called, the appropriate command is executed.

The Command pattern allows you to encapsulate requests as objects and pass them around, which can make the code more flexible and easier to maintain. Additionally, the Command pattern makes it easier to implement undo and redo functionality, as the history of commands can be kept in a stack.


* Disadvantages:

The disadvantages of the Command pattern include:

  1. Increased complexity: The use of the Command pattern can add additional levels of complexity to a software system, especially when it comes to handling multiple commands.
  2. Increased number of objects: The Command pattern can result in a large number of objects in the system, which can be challenging to manage and maintain.
  3. Decreased performance: The use of the Command pattern can result in decreased performance due to the overhead of creating and managing command objects.
  4. Difficulty in undo/redo operations: Implementing undo/redo operations with the Command pattern can be challenging, as it requires the ability to undo and reapply commands in the correct order.

------------------------

(C) Mediator Pattern


The Mediator pattern is a behavioral design pattern that provides a centralized communication channel between objects. The goal of the Mediator pattern is to reduce coupling between objects and promote loose coupling. This is achieved by encapsulating the communication logic between objects within a mediator object.

Here is a simple code sample of the Mediator pattern in Kotlin:

interface ChatRoomMediator {
    fun showMessage(user: User, message: String)
}

class ChatRoom : ChatRoomMediator {
    override fun showMessage(user: User, message: String) {
        println("${user.name} : $message")
    }
}

class User(var name: String, var mediator: ChatRoomMediator) {
    fun send(message: String) {
        mediator.showMessage(this, message)
    }
}

fun main(args: Array<String>) {
    val mediator = ChatRoom()
    val john = User("John", mediator)
    val jane = User("Jane", mediator)

    john.send("Hi there!")
    jane.send("Hey!")
}


In this example, the ChatRoom class acts as the mediator, which handles communication between the User objects. The User objects communicate with each other through the mediator, which encapsulates the communication logic and reduces the coupling between the objects.

Note: This is just a basic example to illustrate the concept of the Mediator pattern. In a real-world scenario, the mediator would typically handle much more complex communication between objects.


* Disadvantages:

The disadvantages of the Mediator pattern are:

  1. Increased Complexity: The Mediator pattern can add complexity to the overall design of the application, as it involves creating additional objects to handle communication between objects.
  2. Hard to Test: The Mediator pattern can make it harder to test individual objects, as the communication logic is encapsulated within the mediator. This requires testing the entire mediator in addition to individual objects, making the testing process more complex.
  3. Increased Coupling: Although the Mediator pattern reduces coupling between objects, it still requires objects to have a reference to the mediator. This increases coupling between the objects and the mediator, making it harder to change the implementation of the mediator without affecting the objects that use it.
  4. Performance Overhead: The Mediator pattern can introduce additional overhead to the system, as communication between objects is routed through the mediator. This can impact performance, especially in high-performance systems.
  5. Tight Coupling with the Mediator: The Mediator pattern can create tight coupling between objects and the mediator. This makes it harder to change or replace the mediator, as objects are tightly coupled to it.

-------------------------

(D) Observer Pattern


The Observer pattern is a behavioral design pattern that defines a one-to-many relationship between objects, where one object (the subject) is being observed by multiple objects (observers). When the subject changes state, it notifies all its observers, who then update their state accordingly.

The key elements of the Observer pattern are:

  1. Subject: The object being observed. It stores a list of observers and provides methods to add and remove observers. It also notifies the observers of any changes to its state.
  2. Observer: The objects observing the subject. It has a method to update its state when the subject changes.
  3. ConcreteSubject: The concrete implementation of the subject. It stores the state of the subject and implements the methods to add and remove observers.
  4. ConcreteObserver: The concrete implementation of the observer. It stores a reference to the subject and implements the update method to update its state when the subject changes.

Here is an example of the Observer pattern in Kotlin:

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

interface Observer {
    fun update(temperature: Int)
}

class WeatherData : Subject {
    private val observers: MutableList<Observer> = mutableListOf()
    private var temperature: Int = 0

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

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

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

    fun setTemperature(temperature: Int) {
        this.temperature = temperature
        notifyObservers()
    }
}

class TemperatureDisplay : Observer {
    private var temperature: Int = 0

    override fun update(temperature: Int) {
        this.temperature = temperature
        display()
    }

    fun display() {
        println("Temperature: $temperature")
    }
}

fun main() {
    val weatherData = WeatherData()
    val temperatureDisplay = TemperatureDisplay()

    weatherData.registerObserver(temperatureDisplay)
    weatherData.setTemperature(20)
    weatherData.setTemperature(25)
}


In this example, the WeatherData object acts as the subject and the TemperatureDisplay object acts as the observer. The TemperatureDisplay object registers itself with the WeatherData object and gets notified of any changes to its temperature. When the temperature changes, the WeatherData object notifies all its observers, and the TemperatureDisplay object updates its display accordingly.


* Disadvantages:

The disadvantages of the Observer pattern are:

  1. Complexity: The Observer pattern can lead to a complex system if not implemented properly. The number of objects and relationships between them can become difficult to manage and maintain.
  2. Performance: The Observer pattern can result in a performance penalty, particularly if it is used in large systems with many observers. Each time a subject changes, it has to notify all its observers, leading to a lot of updates and events that can slow down the system.
  3. Coupling: The Observer pattern can increase coupling between objects, as they are tightly coupled with the subject they are observing. This makes it more difficult to make changes to the system without affecting the observer.
  4. Inflexibility: If the observer is dependent on the subject, it can become inflexible and difficult to change. It can also become difficult to add new observers or remove existing ones without affecting the subject.
  5. Unnecessary updates: Observers can receive updates even if they are not needed. This can result in unnecessary computation, leading to decreased performance.

--------------------

(E) Template Pattern


The Template pattern is a behavioral design pattern that defines the skeleton of an algorithm in a method, called a template method, which defers some steps to subclasses. It lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.

Here’s an example of the Template pattern in Kotlin:

abstract class Game {
    fun play() {
        initialize()
        startPlay()
        endPlay()
    }
    protected abstract fun initialize()
    protected abstract fun startPlay()
    protected abstract fun endPlay()
}

class Cricket : Game() {
    override fun initialize() {
        println("Cricket Game Initialized! Start playing.")
    }
    override fun startPlay() {
        println("Cricket Game Started. Enjoy the game!")
    }
    override fun endPlay() {
        println("Cricket Game Finished!")
    }
}

class Football : Game() {
    override fun initialize() {
        println("Football Game Initialized! Start playing.")
    }
    override fun startPlay() {
        println("Football Game Started. Enjoy the game!")
    }
    override fun endPlay() {
        println("Football Game Finished!")
    }
}

fun main(args: Array<String>) {
    val cricket = Cricket()
    cricket.play()
    println()
    val football = Football()
    football.play()
}


In this example, the Game class defines the template method play(), which calls the three methods initialize(), startPlay(), and endPlay() in a specific order. The Cricket and Football classes extend the Game class and implement the three methods. When the play() method is called, the algorithm is executed and the steps are performed in the order defined in the template method.


* Disadvantages:

The disadvantages of the Template Method pattern are:

  1. Inflexibility: It is not flexible enough as it specifies a fixed structure for an algorithm and this structure cannot be changed easily.
  2. Complexity: It can make the code more complex if it is used improperly.
  3. Rigidity: This pattern is often considered too rigid, as it forces the developer to use a specific structure for the algorithm, and this can lead to a lack of creativity.
  4. Increased Code Duplication: In some cases, the same code blocks can be repeated in different templates, leading to increased code duplication.
  5. Hard to maintain: If the algorithm changes frequently, it becomes harder to maintain the code because every change will have to be made in all the templates that use the same structure.

More Posts