Skip to main content
Create your own
Lesson illustration

Dynamic Behavior with the Decorator Pattern

Hello! Welcome back to our module on Structural Design Patterns.

In our previous lesson, we explored the Adapter pattern, which acts as a bridge to make incompatible interfaces work together. We saw how it can cleanly integrate legacy code or third-party libraries without modifying existing client code.

Today, we'll examine another powerful structural pattern. Your learning outcome is to apply the Decorator pattern to add behavior to objects dynamically.

Where the Adapter pattern changes an object's interface, the Decorator pattern enriches an object's functionality. It allows you to add new responsibilities to an object flexibly and dynamically, without altering its underlying class or the interfaces it implements. This is a key tool for creating scalable and maintainable systems.

The Problem: When Inheritance Becomes a Burden

Imagine you're building a notification system. The initial requirement is to send email notifications. Simple enough. But soon, product managers want to add notifications via SMS, WhatsApp, and Facebook. A user might want to receive only an email, another might want email and SMS, and a third might want all four.

If you were to use inheritance to solve this, you'd quickly run into a "class explosion." You'd need classes like EmailAndSmsNotifier, EmailAndWhatsAppNotifier, EmailSmsAndWhatsAppNotifier, and so on. This approach is rigid, unmanageable, and violates the Open/Closed Principle.

To see this problem in action, let's watch a short segment from a video by Geekific.

The Decorator Pattern Explained and Implemented in Java | Structural Design Patterns | Geekific

This video introduces the notification service problem and clearly demonstrates why using inheritance to add combinations of features leads to an unmanageable number of subclasses.

Please watch from the beginning until 01:56. Focus on how quickly the class hierarchy grows as new notification options are introduced.

This is precisely the kind of inflexible design we want to avoid. The Decorator pattern offers a much more elegant solution.

The Decorator Solution: Wrapping Objects

The Decorator pattern solves this by letting you attach new behaviors to an object by placing it inside a special "wrapper" object. You can have many wrappers, each adding a new responsibility. The key is that both the original object and all the wrappers share the same interface.

Think of ordering a coffee or a pizza. You start with a base item (a plain coffee or a margherita pizza) and then "decorate" it with extras like milk, sugar, cheese, or olives. Each extra adds to the cost and description without changing what a "pizza" or "coffee" fundamentally is.

Let's look at another analogy, this time with a customizable burger, to solidify this core concept.

Decorator Design Pattern | Low Level Design | OOPS | Java | Best Video to Understand

This video by Riddhi Dutta uses a relatable burger shop example to explain the core idea of decorating a base object with various add-ons.

Watch the segment from 00:31 to 02:38. Notice how the total cost is calculated by combining the base price with the price of each added topping.

Structure of the Decorator Pattern

The pattern has four main participants that work together to achieve this "wrapping" effect.

  1. Component: An interface or abstract class that defines the methods that will be implemented by both the concrete components and the decorators. (e.g., Pizza, Coffee, Notifier).
  2. Concrete Component: The base object to which we want to add new functionality. It implements the Component interface. (e.g., MargheritaPizza, PlainCoffee, EmailNotifier).
  3. Decorator: An abstract class that also implements the Component interface. It holds a reference (using composition) to a Component object. This is the "wrapper".
  4. Concrete Decorator: These are the classes that contain the additional responsibilities. They extend the abstract Decorator and add their own behavior before or after delegating the call to the wrapped Component object. (e.g., CheeseTopping, MilkDecorator, SmsDecorator).

This UML diagram visualizes the structure using the coffee example:

Class Diagram of Decorator Design Pattern (Coffee Example)
This diagram shows the 'Coffee' interface implemented by the 'PlainCoffee' (Concrete Component) and the 'CoffeeDecorator'. Concrete decorators like 'MilkDecorator' and 'SugarDecorator' extend 'CoffeeDecorator' and wrap a 'Coffee' object to add functionality.

Java Implementation Walkthrough

Now, let's see how this structure is implemented in Java. We'll continue with the burger example, which clearly demonstrates how to calculate costs and build descriptions by chaining decorators.

Decorator Design Pattern | Low Level Design | OOPS | Java | Best Video to Understand

The same video from Riddhi Dutta provides a full code walkthrough. Pay close attention to how the decorators are 'stacked' on top of the base burger object in the client code.

Please watch from 06:23 to 11:48. In the code walkthrough (06:23 - 09:53), focus on the Burger abstract class, the ZingerBurger concrete component, the BurgerDecorator, and the ExtraCheeseBurger concrete decorator. Notice how the decorator holds an instance of Burger and uses it in its getCost() and getDescription() methods. In the driver code demonstration (09:53 - 11:48), observe how a ZingerBurger object is first created and then progressively wrapped by ExtraCheeseBurger and ExtraMayoBurger. This highlights the dynamic nature of the pattern.

As you saw, the client code can mix and match decorators at runtime to create different combinations. This is possible because both the base ZingerBurger and the decorators (ExtraCheeseBurger, ExtraMayoBurger) are treated as the same type (Burger) through polymorphism.

How the Call Stack Works

When you call a method on the outermost decorator, it creates a chain of calls that goes all the way down to the original object.

Let's use a pizza example to trace the execution. If you have an object created like this:
Pizza pizza = new OliveTopping(new CheeseTopping(new MargheritaPizza()));

A call to pizza.getCost() would work as follows:

  1. OliveTopping.getCost() is called. It returns 30.0 + pizza.getCost().
  2. To resolve pizza.getCost(), it calls CheeseTopping.getCost() on the wrapped object.
  3. CheeseTopping.getCost() returns 50.0 + pizza.getCost().
  4. To resolve this pizza.getCost(), it calls MargheritaPizza.getCost() on the object it wraps.
  5. MargheritaPizza.getCost() returns the base cost, 200.0.
  6. The calls unwind: 50.0 + 200.0 = 250.0.
  7. And again: 30.0 + 250.0 = 280.0.

Decorator Design Pattern in Java – Complete Guide

This article provides a clear, step-by-step explanation of this call flow, along with a complete Java implementation for the pizza example.

Please read the sections 'Java Implementation (Pizza Example)' and 'How the Decorator Pattern Works (Step by Step)'. This will reinforce the code structure and the 'onion-like' wrapping mechanism.

Test your understanding!

You are building a simple text editor. You have a Text interface with a getContent() method, and a PlainText class that implements it.

public interface Text {
    String getContent();
}

public class PlainText implements Text {
    private String content;

    public PlainText(String content) {
        this.content = content;
    }

    @Override
    public String getContent() {
        return content;
    }
}

How would you implement a BoldText decorator that wraps a Text object and adds HTML-style <b> tags around the content? Describe the BoldText class structure.

Show answer

You would first create an abstract TextDecorator and then the concrete BoldText decorator.

  1. Abstract Decorator (TextDecorator): This class implements the Text interface and holds a reference to a Text object.

    public abstract class TextDecorator implements Text {
        protected Text decoratedText;
    
        public TextDecorator(Text text) {
            this.decoratedText = text;
        }
    
        @Override
        public String getContent() {
            return decoratedText.getContent(); // Delegate the call
        }
    }
    
  2. Concrete Decorator (BoldText): This class extends TextDecorator and adds the bolding behavior.

    public class BoldText extends TextDecorator {
        public BoldText(Text text) {
            super(text);
        }
    
        @Override
        public String getContent() {
            // Add new behavior around the original call
            return "<b>" + super.getContent() + "</b>";
        }
    }
    

Usage:

Text myText = new PlainText("Hello World");
System.out.println(myText.getContent()); // Output: Hello World

Text boldText = new BoldText(myText);
System.out.println(boldText.getContent()); // Output: <b>Hello World</b>

You could then create other decorators like ItalicText and combine them: new ItalicText(new BoldText(myText)).

Advantages and When to Use It

The Decorator pattern is particularly useful because it aligns perfectly with the Open/Closed Principle: you can extend an object's behavior without modifying its source code.

Decorator Design Pattern in Java – Complete Guide

Let's review the key advantages and the ideal scenarios for using this pattern. This will help you identify opportunities to apply it in your own designs.

Read the 'Advantages' and 'When to Use It' sections. Also, take a look at the 'Real World Examples in Java API' section.

A classic example from the Java Development Kit (JDK) that you've likely used is the java.io package. A FileInputStream (a concrete component) can be wrapped by a BufferedInputStream (a decorator that adds buffering) which can then be wrapped by a DataInputStream (a decorator that adds methods for reading primitive data types).

new DataInputStream(new BufferedInputStream(new FileInputStream("myFile.txt")));

This is the Decorator pattern in action, allowing you to compose I/O behaviors flexibly.

Conclusion

The Decorator pattern provides a flexible alternative to subclassing for extending functionality. By wrapping objects, you can add new behaviors at runtime, combine them in various ways, and adhere to core design principles.

Key Takeaways:

  • Purpose: To add responsibilities to objects dynamically and transparently.
  • Structure: It uses a chain of "wrapper" objects (Decorators) around a core "wrapped" object (Component), all sharing a common interface.
  • Mechanism: It relies on composition and delegation. Each decorator adds its behavior and then delegates the call to the object it wraps.
  • Primary Benefit: It follows the Open/Closed Principle, allowing for extension without modification, and avoids the "class explosion" problem of using inheritance for feature combinations.

In our next lesson, we will look at the Facade pattern. While a Decorator adds behavior to an object, a Facade provides a simplified, high-level interface to a complex subsystem. It's about reducing complexity, not adding functionality.

Can't find a good explanation? Sign up and we'll make it for you

Sign up