Skip to content
Dev Dump

Behavioral Patterns

Behavioral patterns take care of effective communication and the assignment of responsibilities between objects.

This pattern defines a family of algorithms, encapsulates each one of them, and makes them interchangeable within that family

The Strategy Pattern is a behavioral design pattern that enables selecting an algorithm’s behavior at runtime.

Here, Client is directly interact with the Abstraction, so we cannot modify or change the strategy at the run time, Because it is closely coupled with the interface

Problem

Here we are separated the client and the Interface with the driver class or context class, it will provide the flexibility to change the strategy at run time

Solution

  1. Context: The class that uses a Strategy.
  2. Strategy: The common interface for all supported algorithms.
  3. Concrete Strategies: Classes that implement the Strategy interface with a particular algorithm.
// Strategy Interface
interface PaymentStrategy {
void pay(int amount);
}
// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
private String name;
private String cardNumber;
private String cvv;
private String expiryDate;
public CreditCardPayment(String name, String cardNumber, String cvv, String expiryDate) {
this.name = name;
this.cardNumber = cardNumber;
this.cvv = cvv;
this.expiryDate = expiryDate;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid with Credit Card");
}
}
class PaypalPayment implements PaymentStrategy {
private String emailId;
private String password;
public PaypalPayment(String emailId, String password) {
this.emailId = emailId;
this.password = password;
}
@Override
public void pay(int amount) {
System.out.println(amount + " paid using Paypal");
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
// Usage:
ShoppingCart cart = new ShoppingCart();
PaymentStrategy creditCardPayment = new CreditCardPayment("John Doe", "1234567890123456", "123", "12/25");
cart.setPaymentStrategy(creditCardPayment);
cart.checkout(100); // Output: 100 paid with Credit Card
PaymentStrategy paypalPayment = new PaypalPayment("john.doe@example.com", "password");
cart.setPaymentStrategy(paypalPayment);
cart.checkout(50); // Output: 50 paid using Paypal
  • Flexibility: New algorithms can be introduced without changing the context.
  • Separation of Concerns: The algorithm implementation is separated from the context.
  • Open/Closed Principle: New strategies can be introduced without modifying existing code.

[!tip] Main Problem to address the statergy pattern It can change the statergy at the run time, it gives the flexibility and code modularity and minimise the coupling

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

  • Event Handling Systems: User interface elements need to respond to user actions.
  • Data Models and Views: When the data model changes, the views displaying this data must be updated.
  • Logging Services: Multiple logging services need to capture and process events from a central event source.
  • Distributed Systems: Different components need to stay in sync with changes happening in other components.

[!note] Problem with Traditional Approach In a traditional approach, notifying individual components directly becomes increasingly challenging as the number of components grows. This leads to several issues:

  1. Scalability: Manually managing notifications to a growing number of components becomes impractical and error-prone.
  2. Tight Coupling: The primary class responsible for the notifications becomes tightly coupled to all the components it needs to notify, making the system less flexible.
  3. Maintenance Overhead: Adding, removing, or modifying components requires changes in the primary class, leading to higher maintenance costs and potential for bugs.

Problems

public interface Observer {
void update(String news);
}
public class EmailSubscriber implements Observer {
private String name;
public EmailSubscriber(String name) {
this.name = name;
}
@Override
public void update(String news) {
System.out.println(name + " received news update via email: " + news);
}
}
public class SmsSubscriber implements Observer {
private String name;
public SmsSubscriber(String name) {
this.name = name;
}
@Override
public void update(String news) {
System.out.println(name + " received news update via SMS: " + news);
}
}
import java.util.ArrayList;
import java.util.List;
public interface Subject {
void addObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
public class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
private String news;
@Override
public void addObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
public void setNews(String news) {
this.news = news;
notifyObservers();
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(news);
}
}
}
public class ObserverPatternExample {
public static void main(String[] args) {
NewsAgency newsAgency = new NewsAgency();
Observer emailSubscriber = new EmailSubscriber("Alice");
Observer smsSubscriber = new SmsSubscriber("Bob");
newsAgency.addObserver(emailSubscriber);
newsAgency.addObserver(smsSubscriber);
newsAgency.setNews("Breaking News: Observer Pattern in Java!");
newsAgency.removeObserver(emailSubscriber);
newsAgency.setNews("Another News: Observer Pattern example completed!");
}
}
  1. Observer: Defines an interface for updating objects.
  2. EmailSubscriber and SmsSubscriber: Implement the Observer interface to handle updates.
  3. Subject: Defines an interface for attaching and detaching observers.
  4. NewsAgency: Implements the Subject interface and notifies observers of state changes.
  5. ObserverPatternExample: Demonstrates how to use the Observer Pattern by creating a news agency and adding/removing observers.
  1. Loose Coupling: The news agency (subject) doesn’t need to know the details of its subscribers (observers).
  2. Dynamic Relationships: Subscribers can be added or removed at runtime.
  3. Separation of Concerns: The news agency focuses on publishing news, while the observers handle the notifications.
  4. Scalability:

The State Pattern allows an object to change its behavior when its internal state changes. It provides a way to manage different behaviors based on the object’s state.

State Pattern

  • Complex Conditional Logic: It eliminates large if-else or switch statements by encapsulating state-specific behavior in separate classes, making the code easier to read, maintain, and extend.
  • Code Duplication: It centralizes state-specific behavior in dedicated classes, reducing duplication and ensuring changes are made in one place.
  • Tight Coupling: It decouples the context object from state-specific logic, allowing new states to be added or modified without affecting the context class.
// Bad Implementation: Vending Machine with if-else logic
public class VendingMachineBad {
private String state; // State is represented as a String
public VendingMachineBad() {
this.state = "NoCoin"; // Initial state
}
public void insertCoin() {
if (state.equals("NoCoin")) {
System.out.println("Coin inserted. Machine is ready to dispense.");
state = "HasCoin";
} else if (state.equals("HasCoin")) {
System.out.println("Coin already inserted. Ready to dispense.");
} else if (state.equals("Dispensed")) {
System.out.println("Please wait. Resetting the machine.");
}
}
public void dispenseItem() {
if (state.equals("NoCoin")) {
System.out.println("Insert a coin first.");
} else if (state.equals("HasCoin")) {
System.out.println("Item dispensed. Thank you!");
state = "Dispensed";
} else if (state.equals("Dispensed")) {
System.out.println("Item already dispensed. Insert a new coin.");
state = "NoCoin";
}
}
}
// Client Code
public class BadStateExample {
public static void main(String[] args) {
VendingMachineBad machine = new VendingMachineBad();
// Test the vending machine
machine.dispenseItem(); // Output: Insert a coin first.
machine.insertCoin(); // Output: Coin inserted. Machine is ready to dispense.
machine.insertCoin(); // Output: Coin already inserted. Ready to dispense.
machine.dispenseItem(); // Output: Item dispensed. Thank you!
machine.dispenseItem(); // Output: Item already dispensed. Insert a new coin.
machine.insertCoin(); // Output: Please wait. Resetting the machine.
}
}
// State Interface
interface State {
void insertCoin(VendingMachine machine);
void dispenseItem(VendingMachine machine);
}
// Concrete State: No Coin State
class NoCoinState implements State {
@Override
public void insertCoin(VendingMachine machine) {
System.out.println("Coin inserted. Machine is ready to dispense.");
machine.setState(new HasCoinState());
}
@Override
public void dispenseItem(VendingMachine machine) {
System.out.println("Insert a coin first.");
}
}
// Concrete State: Has Coin State
class HasCoinState implements State {
@Override
public void insertCoin(VendingMachine machine) {
System.out.println("Coin already inserted. Ready to dispense.");
}
@Override
public void dispenseItem(VendingMachine machine) {
System.out.println("Item dispensed. Thank you!");
machine.setState(new DispensedState());
}
}
// Concrete State: Dispensed State
class DispensedState implements State {
@Override
public void insertCoin(VendingMachine machine) {
System.out.println("Please wait. Resetting the machine.");
}
@Override
public void dispenseItem(VendingMachine machine) {
System.out.println("Item already dispensed. Insert a new coin.");
machine.setState(new NoCoinState());
}
}
// Context: Vending Machine
class VendingMachine {
private State state;
// Initial state is NoCoinState
public VendingMachine() {
this.state = new NoCoinState();
}
public void setState(State state) {
this.state = state;
}
public void insertCoin() {
state.insertCoin(this);
}
public void dispenseItem() {
state.dispenseItem(this);
}
}
// Client Code
public class StatePatternExample {
public static void main(String[] args) {
VendingMachine machine = new VendingMachine();
// Test the vending machine
machine.dispenseItem(); // Output: Insert a coin first.
machine.insertCoin(); // Output: Coin inserted. Machine is ready to dispense.
machine.insertCoin(); // Output: Coin already inserted. Ready to dispense.
machine.dispenseItem(); // Output: Item dispensed. Thank you!
machine.dispenseItem(); // Output: Item already dispensed. Insert a new coin.
machine.insertCoin(); // Output: Please wait. Resetting the machine.
}
}

TThe Command Pattern is a behavioral design pattern that turns a request into a standalone object containing all the information about the request.

Command Pattern

  • Decoupling: The sender of a request is separated from the receiver, making the system more flexible and easier to maintain.
  • Flexibility: Commands can be easily added, modified, or removed without affecting the client code.
  • Undo/Redo: Commands can be stored and replayed, allowing for undo/redo functionality.
  • Logging: Commands can be logged for audit or debugging purposes.
  • Asynchronous Processing: Commands can be executed asynchronously, allowing for non-blocking operations.

A Simple Remote Control

Let’s model a simple remote control that can control a light. The remote control has a button to turn the light on and a button to turn the light off.

public class Light {
private boolean isOn = false;
public void turnOn() {
isOn = true;
System.out.println("Light is ON");
}
public void turnOff() {
isOn = false;
System.out.println("Light is OFF");
}
}
public class RemoteControl {
private Light light;
public RemoteControl(Light light) {
this.light = light;
}
public void pressOnButton() {
light.turnOn();
}
public void pressOffButton() {
light.turnOff();
}
public static void main(String[] args) {
Light light = new Light();
RemoteControl remote = new RemoteControl(light);
remote.pressOnButton();
remote.pressOffButton();
}
}
  • Tight Coupling: The Re5nted to control a different type of device (e.g., a fan), you would need to modify the RemoteControl class.
  • Lack of Flexibility: Adding new actions (e.g., dimming the light) would require modifying the RemoteControl class.
  • No Undo/Redo: There’s no easy way to implement undo/redo functionality.
  • Limited Extensibility: You can’t easily extend the remote to control multiple lights or other devices.
// Command Interface
interface Command {
void execute();
void undo(); // Optional: For undoable commands
}
// Concrete Commands
class TurnOnLightCommand implements Command {
private Light light;
public TurnOnLightCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
@Override
public void undo() {
light.turnOff();
}
}
class TurnOffLightCommand implements Command {
private Light light;
public TurnOffLightCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
@Override
public void undo() {
light.turnOn();
}
}
// Receiver
public class Light {
private boolean isOn = false;
public void turnOn() {
isOn = true;
System.out.println("Light is ON");
}
public void turnOff() {
isOn = false;
System.out.println("Light is OFF");
}
}
// Invoker
public class RemoteControl {
private Command onCommand;
private Command offCommand;
private Command lastCommand; // For undo
public RemoteControl(Command onCommand, Command offCommand) {
this.onCommand = onCommand;
this.offCommand = offCommand;
this.lastCommand = null;
}
public void pressOnButton() {
onCommand.execute();
lastCommand = onCommand;
}
public void pressOffButton() {
offCommand.execute();
lastCommand = offCommand;
}
public void pressUndoButton() {
if (lastCommand != null) {
lastCommand.undo();
lastCommand = null;
}
}
public static void main(String[] args) {
Light light = new Light();
Command turnOnCommand = new TurnOnLightCommand(light);
Command turnOffCommand = new TurnOffLightCommand(light);
RemoteControl remote = new RemoteControl(turnOnCommand, turnOffCommand);
remote.pressOnButton();
remote.pressOffButton();
remote.pressUndoButton(); // Turns the light back on
}
}
  • Command Interface: Defines the execute() and undo() methods that all command objects must implement. The undo() method is optional but useful for implementing undoable operations.
  • TurnOnLightCommand and TurnOffLightCommand Classes: These are concrete implementations of the Command interface. Each command encapsulates the action of turning the light on or off, respectively. They hold a reference to the Light object they will control.
  • Light Class (Receiver): This is the object that performs the actual action. The commands delegate the execution to the receiver.
  • RemoteControl Class (Invoker): This class holds the commands and triggers their execution. It doesn’t need to know anything about the specific commands or the receiver; it simply calls the execute() method of the command object. The RemoteControl stores the last executed command, enabling the undo() functionality.