Decorator Pattern

The Decorator Pattern in Java is a structural design pattern that allows behavior to be added to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. It’s useful for extending functionalities of objects in a flexible and reusable manner.

Important Components

  1. Component: An abstract class or interface defining the core functionality of objects.
  2. Concrete Component: A class implementing the Component interface, representing the base object to be decorated.
  3. Decorator: An abstract class or interface that implements the Component interface and holds a reference to a Component object, forwarding calls to it while adding extra behavior.
  4. Concrete Decorator: A class that extends the Decorator, adding specific functionality before or after delegating to the wrapped Component.

How It Works

  • The Component interface defines the operations that can be performed.
  • The Concrete Component provides the base implementation.
  • The Decorator implements the same interface and wraps a Component, delegating operations to it.
  • The Concrete Decorator adds new behavior (e.g., logging, validation) before or after calling the wrapped Component’s methods.
  • Decorators can be stacked, allowing multiple behaviors to be added in a chain.

Example in Java

Let’s model a coffee shop where a base coffee (component) can be enhanced with add-ons like milk or sugar (decorators).

// Component Interface
interface Coffee {
    String getDescription();
    double getCost();
}

// Concrete Component
class SimpleCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Simple Coffee";
    }

    @Override
    public double getCost() {
        return 2.00;
    }
}

// Decorator
abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription();
    }

    @Override
    public double getCost() {
        return coffee.getCost();
    }
}

// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Milk";
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 0.50;
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Sugar";
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 0.20;
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        // Base coffee
        Coffee coffee = new SimpleCoffee();
        System.out.println(coffee.getDescription() + " $" + coffee.getCost());

        // Coffee with milk
        Coffee milkCoffee = new MilkDecorator(coffee);
        System.out.println(milkCoffee.getDescription() + " $" + milkCoffee.getCost());

        // Coffee with milk and sugar
        Coffee milkSugarCoffee = new SugarDecorator(new MilkDecorator(coffee));
        System.out.println(milkSugarCoffee.getDescription() + " $" + milkSugarCoffee.getCost());
    }
}
/*
Simple Coffee $2.0
Simple Coffee, Milk $2.5
Simple Coffee, Milk, Sugar $2.7
*/Code language: PHP (php)

Use case and Implementation

Adding additional features to `Account` objects, such as overdraft protection or account alerts, without modifying the underlying account class.

//DecoratorPatternDemo.java
interface Account {
    void deposit(double amount);
    void withdraw(double amount);
    double getBalance();
}
class SavingsAccount implements Account {
    private double balance;

    public SavingsAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    @Override
    public void deposit(double amount) {
        balance += amount;
        System.out.println(amount + " deposited. Current balance: " + balance);
    }

    @Override
    public void withdraw(double amount) {
        if (balance >= amount) {
            balance -= amount;
            System.out.println(amount + " withdrawn. Current balance: " + balance);
        } else {
            System.out.println("Insufficient funds.");
        }
    }

    @Override
    public double getBalance() {
        return balance;
    }
}

interface AccountDecorator extends Account {
}

class OverdraftAccount implements AccountDecorator {
    private Account account;
    private double overdraftLimit;

    public OverdraftAccount(Account account, double overdraftLimit) {
        this.account = account;
        this.overdraftLimit = overdraftLimit;
    }

    @Override
    public void deposit(double amount) {
        account.deposit(amount);
    }

    @Override
    public void withdraw(double amount) {
        if (account.getBalance() >= amount || (account.getBalance() - amount) >= -overdraftLimit) {
            account.withdraw(amount);
        } else {
            System.out.println("Overdraft limit exceeded.");
        }
    }

    @Override
    public double getBalance() {
        return account.getBalance();
    }
}

class AlertAccount implements AccountDecorator {
    private Account account;

    public AlertAccount(Account account) {
        this.account = account;
    }

    @Override
    public void deposit(double amount) {
        account.deposit(amount);
        System.out.println("Alert: Deposit of " + amount + " made.");
    }

    @Override
    public void withdraw(double amount) {
        account.withdraw(amount);
        System.out.println("Alert: Withdrawal of " + amount + " made.");
    }

    @Override
    public double getBalance() {
        return account.getBalance();
    }
}
public class DecoratorPatternDemo {
    public static void main(String[] args) {
        // Create a basic SavingsAccount
        Account basicAccount = new SavingsAccount(1000);

        // Add overdraft protection
        Account overdraftAccount = new OverdraftAccount(basicAccount, 500);

        // Add account alerts
        Account alertAccount = new AlertAccount(overdraftAccount);

        // Use the enhanced account
        alertAccount.deposit(200);
        alertAccount.withdraw(1500);
        alertAccount.withdraw(100);
    }
}

/*
C:\>javac DecoratorPatternDemo.java

C:\>java DecoratorPatternDemo
200.0 deposited. Current balance: 1200.0
Alert: Deposit of 200.0 made.
Insufficient funds.
Alert: Withdrawal of 1500.0 made.
100.0 withdrawn. Current balance: 1100.0
Alert: Withdrawal of 100.0 made.
*/

Pros

  • Flexibility: Add or remove responsibilities dynamically by stacking decorators.
  • Open/Closed Principle: Extends functionality without modifying existing code.
  • Reusability: Decorators can be reused across different objects implementing the same interface.
  • Single Responsibility: Each decorator focuses on a specific feature.

Cons

  • Complexity: Multiple decorators can make the code harder to understand and debug.
  • Object Proliferation: Stacking many decorators creates numerous small objects.
  • Configuration Overhead: Managing decorator chains can be cumbersome.

When to Use

  • When you need to add responsibilities to objects dynamically and transparently.
  • When extending a class via inheritance is impractical (e.g., too many combinations).
  • When you want to keep new functionality separate and reusable.
  • When the system needs to support optional or combinable features.

Real-World Example

  • Java I/O Streams: BufferedInputStream and DataInputStream are decorators that wrap an InputStream to add buffering or data parsing.
  • GUI Frameworks: Adding borders, scrollbars, or tooltips to UI components dynamically.
  • Web Middleware: Adding logging, authentication, or compression to HTTP request handlers.

The Decorator Pattern is a structural design pattern that allows behavior to be added to individual objects dynamically, without altering the original class structure. In Java, this pattern is ideal for enhancing the functionality of objects in a flexible and reusable manner—commonly used in input/output streams, GUI components, and feature-rich object models.

Scroll to Top