The decorator pattern is a structural pattern that used to add behaviour to an object dynamically without altering its structure and classes, since decorator pattern is based on the principle of using composition instead of inheritance (inheritance comes with flaws). The decorator pattern places these objects inside a special wrapper objects allows implementing new behaviours by adding or overriding methods.

Applicability

  • When you need to add additional responsibilities to individual objects dynamically without affecting other objects from the same class.
  • You want to avoid using numerous subclasses in order to implement combined features or behaviours. The decorator pattern provides a more modular and maintainable solution.

Pros and cons

Advantages

  • More flexible and less complexity than static inheritance
    • Can add or remove behaviours to objects at runtime.
    • Inheritance requires adding new classes for each new or altered behaviour (more complexity).
  • Avoid feature-laden (heavily-loaded) classes high up in the hierarchy
    • Define a simple class and add functionalities incrementally with decorators, application do not need to have un-needed features.
    • You can have decorators independent from the inheritance hierarchy classes, useful for unforeseen extensions.

Drawbacks

  • Decorator and its component are not identical
    • Decorated component is not identical to the component itself, you should not rely on object identity when you use decorator. In other words, two objects of a same type could actually be different, because one might have been decorated.
  • Many little objects
    • Can become hard to learn and debug when lots of objects looks alike (chain of multiple decorated objects). Tracing a long chain of decorators can be time-consuming.
    • Over-customisation of objects can lead to poor maintainability of the codebase.

Best practices

  • Keep decorator chain reasonably simple and avoid excessive nesting of decorators.
  • Document the decorator classes clearly, so that developers can understand how different decorators applied. (including decorator naming)
  • Be cautious when relying on object identify for comparison, and consider using other means of identifying objects when needed.

Approach

  1. Define a component abstraction that represent the type of objects you want to decorate.
  2. Create or identify concrete components that implement the component abstraction. These concrete component objects are what you might want to decorate.
  3. Create a decorator abstraction that also implements or extends the component abstraction. This decorator abstraction maintains a reference to an instance of a concrete component (usually passed by the constructor) and delegates methods calls to the wrapped component.
  4. Create concrete decorators by implementing the decorator abstraction. Each concrete decorator wraps another decorator or a concrete component (using constructors). This allows you to add multiple layers of behaviours to an object.

Components

  • Component (abstract)
    • Define an abstraction for objects you want to decorate or add responsibilities statically or dynamically.
  • Concrete component
    • Defines objects to which additional responsibilities can be attached.
  • Decorator (abstract)
    • Maintains a reference to a component object, defines an abstraction that conforms to (extends or implements) component’s interface or abstract class.
  • Concrete decorator
    • Adds responsibilities to a concrete component or concrete decorator.

Example

The Coffee example presents the concept of using decorator pattern to decorate a coffee object.

Component

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

Concrete component

The concrete components implements the component interface. They are the subjects that can later be decorated.

class SimpleCoffee implements Coffee {
    @Override
    public double cost() {
        return 2.0; // Basic coffee cost
    }
 
    @Override
    public String getDescription() {
        return "Simple Coffee";
    }
}

Decorator

The decorator abstraction implements the component class

// Decorator abstract class
abstract class CoffeeDecorator implements Coffee {
	// maintains a reference to a component object or an already decorated compoent
    protected Coffee decoratedCoffee;
 
	// the component is passed via the decorator constructor
    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }
 
	// delegate method calls to the wrapped component
    @Override
    public double cost() {
        return decoratedCoffee.cost();
    }
 
    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription();
    }
}

Concrete decorator

// Concrete decorator
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }
 
    @Override
    public double cost() {
	    // super refers to the wrapped object (the object being decorated)
        return super.cost() + 1.0; // Add the cost of milk
    }
 
    @Override
    public String getDescription() {
        return super.getDescription() + ", with Milk";
    }
}
 
// Concrete decorator
class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }
 
    @Override
    public double cost() {
        return super.cost() + 0.5; // Add the cost of sugar
    }
 
    @Override
    public String getDescription() {
        return super.getDescription() + ", with Sugar";
    }
}

Client

public class DecoratorExample {
    public static void main(String[] args) {
	    // create the simple coffee
        Coffee coffee = new SimpleCoffee();
        System.out.println("Cost: $" + coffee.cost() + ", Description: " + coffee.getDescription());
 
		// decorate the simple coffee with milk 
        coffee = new MilkDecorator(coffee);
        System.out.println("Cost: $" + coffee.cost() + ", Description: " + coffee.getDescription());
 
		// decorate the already decorated coffee with sugar
        coffee = new SugarDecorator(coffee);
        System.out.println("Cost: $" + coffee.cost() + ", Description: " + coffee.getDescription());
    }
}
OUTPUT:
Cost: $2.0, Description: Simple Coffee
Cost: $3.0, Description: Simple Coffee, with Milk
Cost: $3.5, Description: Simple Coffee, with Milk, with Sugar

Design_PatternStructural_Design_PatternsDecorator_patternSOFT2201