Design patterns : Part 3 — Behavioural

Solutions to common reoccurring design problems

Adil Khan
9 min readAug 22, 2024
Photo by Rod Long on Unsplash

In the previous article we discussed about Structural design patterns that that assembles object into larger and maintainable structures. In this article, we are going to explore behavioural design patterns.

Behavioural Design patterns describe how classes and objects interact and communicate with each other.

There are seven popular behavioural patterns :

  • Observer
  • Strategy
  • Chain of Responsibility
  • Iterator
  • Template
  • Command
  • Mediator

Observer

  • The observer pattern defines one to many dependency between objects where one object(the subject) notifies multiple observers about its state changes.
  • Promotes decoupling between subjects and observers.
  • Supports dynamic addition and removal of observers at runtime.
  • Commonly used for event handling systems and UI components.
interface Observer {
void update(Order order);
}

class Customer implements Observer {
private String name;

public Customer(String name) {
this.name = name;
}

@Override
public void update(Order order) {
System.out.println("Hello, " + name + "! Order #" + order.getId() + " is now " + order.getStatus() + ".");
}
}

class Restaurant implements Observer {
private String restaurantName;

public Restaurant(String name) {
this.restaurantName = name;
}

@Override
public void update(Order order) {
System.out.println("Restaurant " + restaurantName + ": Order #" + order.getId() + " is now " + order.getStatus() + ".");
}
}

class DeliveryDriver implements Observer {
private String driverName;

public DeliveryDriver(String name) {
this.driverName = name;
}

@Override
public void update(Order order) {
System.out.println("Driver " + driverName + ": Order #" + order.getId() + " is now " + order.getStatus() + ".");
}
}

class Order {
private int id;
private String status;
private List<Observer> observers = new ArrayList<>();

public Order(int id) {
this.id = id;
this.status = "Order Placed";
}

public int getId() {
return id;
}

public String getStatus() {
return status;
}

public void setStatus(String newStatus) {
status = newStatus;
notifyObservers();
}

public void attach(Observer observer) {
observers.add(observer);
}

public void detach(Observer observer) {
observers.remove(observer);
}

public void notifyObservers() {
for (Observer observer : observers) {
observer.update(this);
}
}
}

public class OrderStatus {
public static void main(String[] args) {
// Create an order
Order order1 = new Order(123);

// Create customers, restaurants, drivers, and a call center to track the order
Customer customer1 = new Customer("Customer 1");
Restaurant restaurant1 = new Restaurant("Rest 1");
DeliveryDriver driver1 = new DeliveryDriver("Driver 1");

// Attach observers to the order
order1.attach(customer1);
order1.attach(restaurant1);
order1.attach(driver1);

// Simulate order status updates
order1.setStatus("Out for Delivery");

// Simulate more order status updates
order1.setStatus("Delivered");
}
}

Strategy

  • In strategy pattern, we create objects which represent various strategies (family of algorithms) and a context object whose behaviour varies as per the strategy object.
  • Flexibility : Allows dynamic selection of algorithm at runtime.
  • Decoupling : Separates algorithm details from the client, promoting low coupling.
  • Extensibility : Easily add new strategies without modifying the existing code.
  • Testability : Simplifies unit testing by replacing strategies with mocks or stubs.
interface PaymentStrategy {
void processPayment(double amount);
}

// Concrete PaymentStrategy classes
class CreditCardPayment implements PaymentStrategy {
public void processPayment(double amount) {
System.out.println("Processing credit card payment of $" + amount);
}
}

class PayPalPayment implements PaymentStrategy {
public void processPayment(double amount) {
System.out.println("Processing PayPal payment of $" + amount);
}
}

// PaymentStrategyFactory
class PaymentStrategyFactory {
public static PaymentStrategy createPaymentStrategy(String paymentMethod) {
if (paymentMethod.equals("CreditCard")) {
return new CreditCardPayment();
} else if (paymentMethod.equals("PayPal")) {
return new PayPalPayment();
} else {
// Default to CreditCardPayment
return new CreditCardPayment();
}
}
}

// PaymentProcessor
class PaymentProcessor {
private PaymentStrategy paymentStrategy;

public PaymentProcessor() {
paymentStrategy = null;
}

public void setPaymentStrategy(String paymentMethod) {
if (paymentStrategy != null) {
// Clean up the previous strategy
paymentStrategy = null;
}
paymentStrategy = PaymentStrategyFactory.createPaymentStrategy(paymentMethod);
}

public void processPayment(double amount) {
if (paymentStrategy != null) {
paymentStrategy.processPayment(amount);
} else {
System.err.println("Payment strategy not set.");
}
}
}

public class PaymentDemo {
public static void main(String[] args) {
PaymentProcessor processor = new PaymentProcessor();

// Use the factory to create payment strategies
processor.setPaymentStrategy("CreditCard");
processor.processPayment(100.0);

processor.setPaymentStrategy("PayPal");
processor.processPayment(50.0);
}
}

Chain of Responsibility

  • The chain of responsibility pattern creates a chain of receiver objects for a request. This pattern decouples sender and receiver of a request based on type of request. Normally each receiver contains reference to another receiver. If one object cannot handle the request then it passes the same to the next receiver and so on.
  • eg. exception handling, security filters, approval workflows.
// Define the abstract base class for order handlers.
abstract class OrderHandler {
protected OrderHandler nextHandler;

public OrderHandler(OrderHandler nextHandler) {
this.nextHandler = nextHandler;
}

public abstract void processOrder(String order);
}

// Concrete handler for order validation.
class OrderValidationHandler extends OrderHandler {
public OrderValidationHandler(OrderHandler nextHandler) {
super(nextHandler);
}

@Override
public void processOrder(String order) {
System.out.println("Validating order: " + order);
// Perform order validation logic here

// If the order is valid, pass it to the next handler
if (nextHandler != null) {
nextHandler.processOrder(order);
}
}
}

// Concrete handler for payment processing.
class PaymentProcessingHandler extends OrderHandler {
public PaymentProcessingHandler(OrderHandler nextHandler) {
super(nextHandler);
}

@Override
public void processOrder(String order) {
System.out.println("Processing payment for order: " + order);
// Perform payment processing logic here

// If payment is successful, pass it to the next handler
if (nextHandler != null) {
nextHandler.processOrder(order);
}
}
}

// Concrete handler for order preparation.
class OrderPreparationHandler extends OrderHandler {
public OrderPreparationHandler(OrderHandler nextHandler) {
super(nextHandler);
}

@Override
public void processOrder(String order) {
System.out.println("Preparing order: " + order);
// Perform order preparation logic here

// If preparation is complete, pass it to the next handler
if (nextHandler != null) {
nextHandler.processOrder(order);
}
}
}

// Concrete handler for delivery assignment.
class DeliveryAssignmentHandler extends OrderHandler {
public DeliveryAssignmentHandler(OrderHandler nextHandler) {
super(nextHandler);
}

@Override
public void processOrder(String order) {
System.out.println("Assigning delivery for order: " + order);
// Perform delivery assignment logic here

// If delivery is assigned, pass it to the next handler
if (nextHandler != null) {
nextHandler.processOrder(order);
}
}
}

// Concrete handler for order tracking.
class OrderTrackingHandler extends OrderHandler {
public OrderTrackingHandler(OrderHandler nextHandler) {
super(nextHandler);
}

@Override
public void processOrder(String order) {
System.out.println("Tracking order: " + order);
// Perform order tracking logic here
}
}

public class SwiggyOrder {
public static void main(String[] args) {
// Create a chain of responsibility for order processing
OrderHandler orderProcessingChain = new OrderValidationHandler(
new PaymentProcessingHandler(
new OrderPreparationHandler(
new DeliveryAssignmentHandler(
new OrderTrackingHandler(null)))));
// The last handler has no next handler

// Simulate an order being placed
String order = "Pizza";
orderProcessingChain.processOrder(order);
}
}

Iterator

  • The iterator pattern is used to provide a uniform way to access the elements of a collection in sequential manner without exposing its underlying structure or implementation.
  • eg. file system, menu system, playlist management.
class Product {
private String name;
private double price;

public Product(String name, double price) {
this.name = name;
this.price = price;
}

public String getName() {
return name;
}

public double getPrice() {
return price;
}
}

// Iterator interface
interface Iterator {
Product first();
Product next();
boolean hasNext();
}

// Concrete iterator for the product collection
class ProductIterator implements Iterator {
private List<Product> products;
private int current;

public ProductIterator(List<Product> products) {
this.products = products;
this.current = 0;
}

public Product first() {
if (products.isEmpty()) {
return null;
}
current = 0;
return products.get(current);
}

public Product next() {
if (hasNext()) {
return products.get(++current);
}
return null;
}

public boolean hasNext() {
return current < products.size() - 1;
}
}

// Aggregate class that stores products and provides an iterator
class Inventory {
private List<Product> products = new ArrayList<>();

public void addProduct(Product product) {
products.add(product);
}

public Iterator createIterator() {
return new ProductIterator(products);
}
}

public class AmazonInventory {
public static void main(String[] args) {
// Create some products
Product product1 = new Product("Laptop", 99999.99);
Product product2 = new Product("Smartphone", 49999.99);
Product product3 = new Product("Headphones", 7999.99);

// Create an inventory and add products
Inventory inventory = new Inventory();
inventory.addProduct(product1);
inventory.addProduct(product2);
inventory.addProduct(product3);

// Create an iterator and iterate over the products
Iterator iterator = inventory.createIterator();
Product currentProduct = iterator.first();

while (currentProduct != null) {
System.out.println("Product: " + currentProduct.getName() + ", Price: $" + currentProduct.getPrice());
currentProduct = iterator.next();
}
}
}

Template

  • In Template pattern, an abstract class exposes a defined way(s)/template(s) to execute its methods. Its subclasses can override the method implementation as per need but the invocation is to be in the same way as defined by the abstract class.
  • Enforces a consistent algorithm structure across subclasses.
  • eg. report generation, test automation.
// Abstract class representing the template for order processing
abstract class OrderProcessingTemplate {
public void processOrder() {
verifyOrder();
assignDeliveryAgent();
trackDelivery();
}

abstract void verifyOrder();
abstract void assignDeliveryAgent();
abstract void trackDelivery();
}

// Concrete subclass for processing orders from local restaurants
class LocalOrderProcessor extends OrderProcessingTemplate {
void verifyOrder() {
System.out.println("Verifying local order...");
// Specific logic for verifying local orders
}

void assignDeliveryAgent() {
System.out.println("Assigning a local delivery agent...");
// Specific logic for assigning local delivery agents
}

void trackDelivery() {
System.out.println("Tracking local delivery...");
// Specific logic for tracking local deliveries
}
}

// Concrete subclass for processing orders from international restaurants
class InternationalOrderProcessor extends OrderProcessingTemplate {
void verifyOrder() {
System.out.println("Verifying international order...");
// Specific logic for verifying international orders
}

void assignDeliveryAgent() {
System.out.println("Assigning an international delivery agent...");
// Specific logic for assigning international delivery agents
}

void trackDelivery() {
System.out.println("Tracking international delivery...");
// Specific logic for tracking international deliveries
}
}

public class AmazonOrderProcessor {
public static void main(String[] args) {
OrderProcessingTemplate localOrder = new LocalOrderProcessor();
OrderProcessingTemplate internationalOrder = new InternationalOrderProcessor();

System.out.println("Processing a local order:");
localOrder.processOrder();
System.out.println();

System.out.println("Processing an international order:");
internationalOrder.processOrder();
}
}

Command

  • The command pattern decouples the sender of a request from its receiver.
  • Decoupling : Separates sender and receiver
  • Flexibility : Allows for dynamic command composition and execution.
//Command Interface
interface ActionListenerCommand {
void execute();
}

//Receiver - performing the operation
class Document {
public void open() {
System.out.println("Document Opened");
}
public void save() {
System.out.println("Document Saved");
}
}

//Concrete Command
class ActionOpen implements ActionListenerCommand {
private Document doc;
public ActionOpen(Document doc) {
this.doc = doc;
}
@Override
public void execute() {
doc.open();
}
}
//Concrete Command
class ActionSave implements ActionListenerCommand {
private Document doc;
public ActionSave(Document doc) {
this.doc = doc;
}
@Override
public void execute() {
doc.save();
}
}
// Invoker
class MenuOptions {
private List<ActionListenerCommand> commands = new ArrayList<>();
public void addCommand(ActionListenerCommand command) {
commands.add(command);
}
public void executeCommands() {
for (ActionListenerCommand command : commands) {
command.execute();
}
}
}
public class DocumentDemo {
public static void main(String[] args) {
Document doc = new Document(); // Receiver - performing action
// Create concrete commands
// Receiver with command
ActionListenerCommand clickOpen = new ActionOpen(doc);
ActionListenerCommand clickSave = new ActionSave(doc);
// Invoker
MenuOptions menu = new MenuOptions();
// Client code only adds commands to the menu
menu.addCommand(clickOpen);
menu.addCommand(clickSave);
menu.executeCommands();
}
}

Mediator

  • Mediator pattern is used to reduce communication complexity between multiple objects or classes. This pattern provides a mediator class which normally handles all the communications between different classes and supports easy maintenance of the code by loose coupling.
  • eg. airplane landing and take off uses control room, ui components for a screen.
// Mediator Interface
interface FormMediator {
void notify(Component component, String event);
void registerComponent(Component component);
void updateComponentState();
}

// Concrete Mediator
class RegistrationFormMediator implements FormMediator {
private NameField nameField;
private EmailField emailField;
private TermsCheckbox termsCheckbox;
private SubmitButton submitButton;

@Override
public void registerComponent(Component component) {
if (component instanceof NameField) {
nameField = (NameField) component;
} else if (component instanceof EmailField) {
emailField = (EmailField) component;
} else if (component instanceof TermsCheckbox) {
termsCheckbox = (TermsCheckbox) component;
} else if (component instanceof SubmitButton) {
submitButton = (SubmitButton) component;
}
}

@Override
public void notify(Component component, String event) {
updateComponentState();
}

@Override
public void updateComponentState() {
boolean isFormValid = !nameField.getText().isEmpty() &&
!emailField.getText().isEmpty() &&
termsCheckbox.isChecked();
submitButton.setEnabled(isFormValid);
}
}

// Abstract Colleague
abstract class Component {
protected FormMediator mediator;

public Component(FormMediator mediator) {
this.mediator = mediator;
mediator.registerComponent(this);
}

public void changed() {
mediator.notify(this, "change");
}
}

// Concrete Colleague for NameField
class NameField extends Component {
private String text = "";

public NameField(FormMediator mediator) {
super(mediator);
}

public void setText(String text) {
this.text = text;
changed();
}

public String getText() {
return text;
}
}

// Concrete Colleague for EmailField
class EmailField extends Component {
private String text = "";

public EmailField(FormMediator mediator) {
super(mediator);
}

public void setText(String text) {
this.text = text;
changed();
}

public String getText() {
return text;
}
}

// Concrete Colleague for TermsCheckbox
class TermsCheckbox extends Component {
private boolean checked = false;

public TermsCheckbox(FormMediator mediator) {
super(mediator);
}

public void setChecked(boolean checked) {
this.checked = checked;
changed();
}

public boolean isChecked() {
return checked;
}
}

// Concrete Colleague for SubmitButton
class SubmitButton extends Component {
private boolean enabled = false;

public SubmitButton(FormMediator mediator) {
super(mediator);
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
System.out.println("SubmitButton is " + (enabled ? "enabled" : "disabled"));
}

public boolean isEnabled() {
return enabled;
}
}

// Client Code
public class MediatorWithUIExample {
public static void main(String[] args) {
FormMediator mediator = new RegistrationFormMediator();

NameField nameField = new NameField(mediator);
EmailField emailField = new EmailField(mediator);
TermsCheckbox termsCheckbox = new TermsCheckbox(mediator);
SubmitButton submitButton = new SubmitButton(mediator);

System.out.println("Initial state:");
mediator.updateComponentState(); // Updates SubmitButton's state

System.out.println("User enters name");
nameField.setText("John Doe");

System.out.println("User enters email");
emailField.setText("john.doe@example.com");

System.out.println("User checks the terms checkbox");
termsCheckbox.setChecked(true);
}
}

I hope you learned something! Thank you for reading.

You can find me on LinkedIn

Click the 👏 to show your support and share it with other fellow Medium users.

--

--

Adil Khan
Adil Khan

Written by Adil Khan

Mobile Application Enthusiast, an Android developer and a keen observer of life

No responses yet