NashTech Blog

Design Patterns: Guide to Behavioural Design Patterns

Table of Contents

What are design patterns?

Reusable solutions to common problems encountered in software development. They are not pieces of code that can be directly copied and used; rather, they are conceptual templates that guide you in solving recurring issues. By applying them, you can improve the quality of your code, making it less complex and more reusable. Additionally, design patterns help ensure your code adheres to the SOLID principles, promoting robust, maintainable and in future scalable software design.

Classification of Design Pattern

design patterns

Behavioral Design Patterns: How objects communicate?

Behavioral patterns focus on defining how objects interact with each other and manage the flow of information between them. These patterns emphasize establishing clear communication while keeping objects loosely coupled, meaning changes in one object have minimal impact on others. Additionally, they help in managing dynamic behavior, such as state transitions, by providing structured ways to handle changes in an object’s state or workflow. This approach enhances the flexibility, scalability, and maintainability of the system by promoting efficient collaboration between entities.

Let’s dive into the world of behavioral design patterns by exploring how each solves a real-life problem.

Memento Pattern

The Memento Design Pattern is used to solve problems where an object’s state needs to be saved and restored later, while ensuring encapsulation is not violated.

Key Components:

  • Originator: The object whose state needs to be saved or restored. It creates a memento containing its internal state.
  • Memento: A snapshot that captures and stores the internal state of the originator. It does not expose the internal state directly to external entities.
  • Caretaker: Manages the mementos and is responsible for saving and restoring them. It does not inspect or modify the contents of the memento.

Here let’s take an example of a simple game where the player’s health and level needs to be saved and restored.

Player(Originator)
public class Player {
   private int health;
   private int level;

   // save player's current state
   public PlayerMemento save(){
       return new PlayerMemento(health, level);
   }

   // restore player's state
   public void restore(PlayerMemento memento){
       health = memento.getHealth();
       level = memento.getLevel();
   }
}
PlayerMemento(Memento)
public class PlayerMemento {
   private final int health;
   private final int level;


   public PlayerMemento(int health, int level) {
       this.health = health;
       this.level = level;
   }
}
Caretaker
public class Caretaker {
   private final Stack<PlayerMemento> mementos = new Stack<>();


   public void saveState(Player player){
       mementos.push(player.save());
   }


   public void undo(Player player){
       if(!mementos.empty()){
           mementos.pop();
           player.restore(mementos.peek());
       }
   }
}

Observer Pattern

The Observer Pattern defines a one-to-many relationship between an object, referred to as the Subject, and its dependents, known as the Observers. Specifically, when the Subject’s state changes, all its dependents are automatically notified, typically through a callback or method invocation. Moreover, this pattern is frequently used for implementing event-distribution systems, especially in scenarios where changes in one object need to be efficiently communicated to other objects. Consequently, the Observer Pattern facilitates clear and dynamic communication between components, ensuring that the system remains cohesive yet loosely coupled.

Key Components

  • Subject: The object being observed.
  • Observant: The dependencies being notified of the change.

Here, let’s take example of stock market where stock prices are being observed by investors.

StockMarket(Subject)		
public class StockMarket {
   private String stock;
   private int price;
   List<Observer> observerList;

   public StockMarket() {
       observerList = new ArrayList<>();
   }

   public void setStockDetails(String stock, int price){
       this.stock = stock;
       this.price = price;
       notifyObservers();
   }

   public void registerObserver(Observer observer){
       observerList.add(observer);
   }

   public void unregisterObserver(Observer observer){
       observerList.remove(observer);
   }

   public void notifyObservers(){
       for (Observer observer: observerList){
           observer.update(stock, price);
      }
   }
}
Observer
public interface Observer {
   void update(String stock, int price);
}
ConcreteObserver1
public class Invester implements Observer{
   @Override
   public void update(String stock, int price) {
       System.out.println("Stock details are: "+stock+" and "+price);
   }
}

ConcreteObserver2
public class Dashboard implements Observer{
   @Override
   public void update(String stock, int price) {
       System.out.println("Stock details are: "+stock+" and "+price);
   }
}

Strategy Pattern

The Strategy pattern is a behavioral design pattern that is commonly used to dynamically switch between different algorithms (strategies). Specifically, it decouples the implementation of these algorithms from the client code, thereby allowing the client to remain independent of specific algorithm details. As a result, the client code does not need modification when new strategies are introduced or existing ones are updated.

On the other hand, in the absence of the Strategy pattern, developers often resort to using extensive if-else or switch statements to achieve similar functionality. Unfortunately, this approach results in tight coupling between the client (context) and the algorithms, which ultimately makes the code more challenging to maintain, extend, and test. However, the Strategy pattern effectively addresses this issue by encapsulating each algorithm within its own class. Moreover, it leverages polymorphism to dynamically select the appropriate strategy at runtime, thus promoting flexibility and reducing complexity.

Key Components

  • Context: The client class that uses a strategy to perform an operation.
  • Strategy Interface: Defines the operations that all concrete strategies must implement.
  • Concrete Strategy: Implements the actual algorithms, interchangeable based on the context.

Lets take an example of an application which needs to support different file compression techniques

FileCompressor(Context)
public class FileCompressor {
   private CompressionStrategy strategy;


   public FileCompressor(CompressionStrategy strategy) {
       this.strategy = strategy;
   }


   public void setStrategy(CompressionStrategy strategy) {
       this.strategy = strategy;
   }


   public void compressFile(String file) {
       strategy.compress(file);
   }
}
CompressionStrategy(Strategy Interface)
public interface CompressionStrategy {
   public void compress(String file);
}
ZipCompression(Concrete Strategy 1)
public class ZipCompression implements CompressionStrategy{
   @Override
   public void compress(String file) {
       System.out.println("Compressing"+ file);
   }
}

RarCompression(Concrete Strategy 2)
public class RarCompression implements CompressionStrategy{
   @Override
   public void compress(String file) {
       System.out.println("Compressing "+ file);
   }
}

Command Pattern

The Command Pattern is a behavioral design pattern that decouples the request for an action from its execution. It encapsulates actions into command objects, allowing you to treat them as first-class objects. By doing so, the actions become independent of the user interface (UI) or the context in which they are invoked, making the code more flexible and extensible.

Key Components

  • Command: The interface that defines operations.
  • Invoker: The object that sends the command. It doesn’t know the details of the action but invokes it.
  • Receiver: The object that knows how to perform the actual action.
  • Concrete Command: Implements the Command interface, binding the Receiver to the action.
Command Interface
public interface Command {
   void execute();
   void undo();
}
Fan(Receiver)
public class Fan {
   public void turnOn() {
       System.out.println("Fan is ON");
   }


   public void turnOff() {
       System.out.println("Fan is OFF");
   }
}
FanOnCommand(Concrete Command)
public class FanOnCommand implements Command {
   private Fan fan;


   public FanOnCommand(Fan fan) {
       this.fan = fan;
   }


   @Override
   public void execute() {
       fan.turnOn();
   }


   @Override
   public void undo() {
       fan.turnOff();
   }
}
Remote Control(Invoker)
public class RemoteControl {
   private Command command;


   public void setCommand(Command command) {
       this.command = command;
   }


   public void pressButton() {
       command.execute();
   }


   public void pressUndo() {
       command.undo();
   }
}

Template Method Pattern

The Template Method Pattern is a behavioral design pattern that defines the skeleton of an algorithm in a base class, thereby allowing subclasses to override specific steps of the algorithm without altering its overall structure. This pattern is particularly useful when certain parts of an algorithm are shared across multiple implementations, whereas other parts need to be customized. Without the Template Method Pattern, code duplication can often occur, as each implementation might independently re-define the shared steps. However, by using this pattern, you encapsulate the invariant steps in the base class and delegate the variant steps to subclasses. Consequently, this ensures a cleaner, more efficient, and maintainable design.

Key Components

  • Abstract Class: Defines the algorithm skeleton.
  • Concrete Subclasses: Override specific steps of the algorithm.
Beverage(Abstract class)
abstract class beverage {
   public final void prepareRecipe() {
       boilWater();
       brew();
   }
   public void boilWater(){
       System.out.println("Boiling water");
   }
   abstract void brew();
}
Coffee(Concrete subclass)
public class coffee extends beverage{
   @Override
   void brew() {
       System.out.println("brewing coffee");
   }
}

Conclusion

Behavioral design patterns provide an ease of object communication. Furthermore, they also reduce code complexity and, additionally, promote reusability. Consequently, applying these patterns ultimately results in efficient, scalable, and maintainable code.

Picture of Jasleen Kaur Wahi

Jasleen Kaur Wahi

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top