2.4 Decorator Pattern - Structural Design Pattern

Design patterns are vital resources in the field of software engineering that help address common design issues in software development. The Decorator Pattern is unique among these patterns in that it is a structural pattern that enables objects to be dynamically extended with new functionalities without requiring changes to their structure. This blog will examine the principles and Java implementation of the Decorator Pattern.

Understanding the Decorator Pattern

Class composition and object composition are the domain of structural design patterns, which includes the Decorator Pattern. With the help of this pattern, objects can have additional responsibilities or behaviors added to them at runtime without altering how they behave during compilation. It encourages the use of open-closed architecture, which makes it simple to add new functionality to classes without changing the existing code.

Key Components

Typically, the Decorator Pattern includes the following elements:

  1. Component: This is an abstract class or base interface that defines the methods that concrete components and decorators will implement.

  2. Concrete Component: This is how the Component interface is implemented in its most basic form. It outlines the fundamental features that decorators can add to.

  3. Decorator: This abstract class keeps track of a reference to an object that is a component and implements the Component interface. Decorators can improve the behavior of the wrapped object because they share the same interface as the Components they decorate.

  4. Concrete Decorator: These are Decorator class subclasses that give the wrapped component particular improvements.

Implementation in Java

Let's illustrate the Decorator Pattern with a simple example in Java. Suppose we have a Coffee interface representing different types of coffee beverages. We'll then implement a basic concrete component, SimpleCoffee, and a set of decorators to add additional features such as milk and sugar to the coffee.

// Step 1: Define the Coffee interface
interface Coffee {
    double getCost();
    String getDescription();
}
// Step 2: Implement the Concrete Component
class SimpleCoffee implements Coffee {
    @Override
    public double getCost() {
        return 1.0;
    }
    @Override
    public String getDescription() {
        return "Simple coffee";
    }
}
// Step 3: Implement the Decorator
abstract class CoffeeDecorator implements Coffee {
    protected final Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }
    @Override
    public double getCost() {
        return decoratedCoffee.getCost();
    }
    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription();
    }
}
// Step 4: Implement Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }
    @Override
    public double getCost() {
        return super.getCost() + 0.5;
    }
    @Override
    public String getDescription() {
        return super.getDescription() + ", with milk";
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }
    @Override
    public double getCost() {
        return super.getCost() + 0.2;
    }
    @Override
    public String getDescription() {
        return super.getDescription() + ", with sugar";
    }
}
// Step 5: Client Code
public class Main {
    public static void main(String[] args) {
        Coffee simpleCoffee = new SimpleCoffee();
        System.out.println("Cost: " + simpleCoffee.getCost() + ", Description: " + simpleCoffee.getDescription());
        Coffee milkCoffee = new MilkDecorator(simpleCoffee);
        System.out.println("Cost: " + milkCoffee.getCost() + ", Description: " + milkCoffee.getDescription());
        Coffee sweetCoffee = new SugarDecorator(milkCoffee);
        System.out.println("Cost: " + sweetCoffee.getCost() + ", Description: " + sweetCoffee.getDescription());
    }
}

Output:

Cost: 1.0, Description: Simple coffee

Cost: 1.5, Description: Simple coffee, with milk

Cost: 1.7, Description: Simple coffee, with milk, with sugar

Conclusion :

One useful method for improving an object's usefulness in a dynamic and adaptable way is the Decorator Pattern. It encourages code reuse, modularity, and scalability in software systems by enabling the addition of new actions to objects during runtime. In this blog, we looked at the main ideas behind the Decorator Pattern and used a basic coffee example to show how it might be used in Java. With the help of this pattern, developers can increase the functionality of their software systems without compromising the maintainability and extensibility of the code.