Structural Design Pattern: Decorator

Photo by Fahmi Fakhrudin on Unsplash

This article is the series exploring design patterns using the Java programming language. The goal of this series is to help readers develop a solid understanding of design patterns while also sharing real-world examples from actual codebases that make use of these patterns.

In this article, we’ll be discussing the Decorator pattern.

Symbols for better navigation:

🤔 Hypothetical Scenario/Imagination

⚠️ Warning

👉 Point to note

📝 Common Understanding

Decorator Pattern

👉 Decorator Design Pattern is used to extend the features of an existing class during runtime by adding more instead of subclassing the existing class.

A common question that might occur is “Isn’t inheritance the same thing?”.

And the answer is yes.

Inheritance is where we take the existing base class and change its behaviour in the superclass. However, this is not flexible when the hierarchy grows.

For example, let’s say we have a class A, and I want to add new features to it. To achieve this, I have to create two subclasses A1 and A2.

But what if a new requirement comes, and I want to add a third feature? I have to deal with creating an A3 class, probably.

Later down the line, after years of extending classes, we will find that there are major deviations in classes A1, A2 and A3 as more classes extended them as base classes and kept adding more features.

To answer this inheritance entanglement, we have the decorator pattern🥳.

Decorators are basically additional features that can be easily added to the base class to extend its functions.

This pattern plays out well where you don’t want to modify the existing code of the legacy class, but want to add more features to it.

This is especially crucial when a hierarchy grows, where introducing new variations becomes messy and produces deeply nested inheritance chains.

Decorator then solves this by letting us wrap objects inside other objects that add functionality.

🤔 Let’s take a real-world analogy of making coffee ☕.

In your programming example, we take Coffee as a class, to which many features can be added, like milk (maybe in varying quantities), sugar, various flavors etc.

Hence, a base coffee can then be made into multiple different drinks:

  • MilkCoffee
  • CaramelMilkSugarCoffee
  • CaramelMilkCoffee

If we did not know about Decorator, we probably would end up creating 3 subclasses 😐 from the base class of Coffee, having different features.

But let’s see how Decorator will solve it.

First, we will have our Coffee class, say Coffee.

Now we will have MilkDecorator, CaramelDecorator, SugarDecorator classes, which are like three extensible features on top of the Coffee class.

In order to make our coffees as listed in the list, we will employ the decorator pattern as follows:

Java
public class CoffeeExample {
    public static void main(String[] args) {
        Coffee c1 = new Coffee(); // Base Coffee

        Coffee c2 = new MilkDecorator(new BasicCoffee()); // MilkCoffee

        Coffee c3 = new CaramelDecorator(new MilkDecorator(new BasicCoffee())); // CaramelMilkCoffee
        
        Coffee c4 = new SugarDecorator(new CaramelDecorator(new MilkDecorator(new Coffee()))); // CaramelMilkSugarCoffee
    }
}

And there you have it 🥳!

All types of coffee without extending the base class.

👉 One thing to note here is that these extensions will be part of runtime operations, hence the creation of the Coffee object with different features can be done on demand.

Recipe to cook the Decorator Pattern for objects

Objects can follow the decorator style of extending features to themselves by following the pattern as follows:

  • Create a concrete base class.
  • Create an abstract decorator class.
  • Implement concrete decorators by defining the behaviour.
  • Wrap the objects with decorators to extend their functionality.

Case-Study

Let’s take the same example of Coffee class to understand this pattern by creating all the artifacts.

We’ll first start with creating our concrete base class.

Coffee.java
public class Coffee {
    public String getDescription() {
        return "Basic Coffee";
    }

    public double getCost() {
        return 50.0;
    }
}

Now, let’s write our abstract decorator class, which will be used to create new features for the coffee.

AbstractCoffeeDecorator.java
public abstract class AbstractCoffeeDecorator implements Coffee {
    protected Coffee coffee;

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

Finally, let’s write our first feature which is Milk.

MilkDecorator.java
public class MilkDecorator extends AbstractCoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Milk"; // Will have milk!
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 10.0; // Will cost 10 more
    }
}

After the creation of these three artifacts, the decorator design pattern will be used as follows.

Java
public class CoffeeExample {
    public static void main(String[] args) {
        Coffee myFavoritCoffee = new MilkDecorator(new Coffee());
    }
}

Real-World Use Case of Proxy

The most widely known implementation is Java’s I/O Streams.

Java
public class TestIoStream {
  public static void main() {
    InputStream input = new BufferedInputStream(new DataInputStream(new FileInputStream("test.txt")));
  }
}

The InputStream class is the main Java class responsible for capturing data.

The features of this class have been extended by the use of decorators such as DataInputStream, BufferedInputStream, etc.

Take a look at the following code snippet for the FilterInputStream class, which is extended on top of the InputStream class.

FilterInputStream extending InputStream

Now, to extend the functionality of the FilterInputStream, the BufferedInputStream class acts as a decorator as follows.

BufferedInputStream as a decorator for the FilterInputStream

⚠️ One common issue with the Decorator design pattern is that every class will need its own set of decorators.

Hence, we might end up creating many decorators, some of which might be achieved using overriding the base class.

Up next in the Series

Flyweight Design Pattern

Subscribe to my newsletter today!

Share it on