Decorator

Structural Design Patterns

Imagine you're building a notification system. You start with a simple Notifier class that sends messages.

Soon, requirements expand. The business wants to add optional ways to send notifications: via SMS, then Facebook, then maybe encrypting the message, or compressing it first. If you use inheritance to handle these combinations, your class hierarchy explodes.

You would need:

  • SmsNotifier, FacebookNotifier, EncryptedNotifier
  • Then combinations: SmsAndFacebookNotifier, EncryptedSmsNotifier
  • And even more complex combinations: EncryptedSmsAndFacebookNotifier...

This approach is unmanageable. The number of classes grows exponentially with each new feature. The hierarchy is rigid, and you violate the Open/Closed Principle every time you need a new combination. How can you add these responsibilities to objects dynamically without creating a new class for every possible feature set?

Attaching Responsibilities Dynamically

The Decorator is a structural design pattern that lets you attach new behaviors to objects by placing them inside special "wrapper" objects that contain the behaviors.

The core idea is to create a set of "decorator" classes that "wrap" the original object. These decorators share the same interface as the object they wrap, so they are transparent to the client. You can "stack" multiple decorators on top of each other, with each one adding its own specific responsibility. It's like putting on clothes: you start with a person (the core object), then you can "decorate" them with a shirt, then a jacket, then a scarf. Each item adds something new without changing the person underneath.

The Participants

  1. Component: The common interface for both the objects being decorated and the decorators themselves. This ensures they are interchangeable. (e.g., INotifier).
  2. Concrete Component: The original, core object that has the basic functionality (e.g., EmailNotifier).
  3. Base Decorator: An abstract class that also implements the Component interface. It holds a reference to a wrapped Component object. Its main purpose is to delegate all method calls to the object it wraps, providing a common base for all concrete decorators.
  4. Concrete Decorators: The classes that add the extra responsibilities. Each one extends the Base Decorator and adds its own behavior either before or after delegating the call to the wrapped object. (e.g., SmsDecorator, EncryptionDecorator).

Example: The Notification System

Let's build our flexible notification system using the Decorator pattern.

Step 1: The Component Interface

public interface INotifier {
    void send(String message);
}

Step 2: The Concrete Component

This is our basic, default notifier.

public class EmailNotifier implements INotifier {
    @Override
    public void send(String message) {
        System.out.println("Sending core notification via Email: " + message);
    }
}

Step 3: The Base Decorator

This abstract class will be the superclass for all our specific decorators.

public abstract class NotifierDecorator implements INotifier {
    protected final INotifier wrappedNotifier;

    public NotifierDecorator(INotifier notifier) {
        this.wrappedNotifier = notifier;
    }

    @Override
    public void send(String message) {
        // Default behavior is to delegate to the wrapped object
        wrappedNotifier.send(message);
    }
}

Step 4: Concrete Decorators

Each decorator adds one specific piece of functionality.

public class SmsDecorator extends NotifierDecorator {
    public SmsDecorator(INotifier notifier) {
        super(notifier);
    }

    @Override
    public void send(String message) {
        super.send(message); // First, do the original notification
        sendSms(message);    // Then, add our new functionality
    }

    private void sendSms(String message) {
        System.out.println("...also sending via SMS: " + message);
    }
}

public class EncryptionDecorator extends NotifierDecorator {
    public EncryptionDecorator(INotifier notifier) {
        super(notifier);
    }

    @Override
    public void send(String message) {
        String encryptedMessage = encrypt(message);
        // We modify the data BEFORE delegating it to the wrapped object
        super.send(encryptedMessage);
    }

    private String encrypt(String message) {
        return "Encrypted(" + message + ")";
    }
}

Step 5: Client Code

The client can now compose complex notification behaviors at runtime.

INotifier notifier = new EmailNotifier(); // Start with the core object
notifier = new SmsDecorator(notifier);     // Wrap it with the SMS decorator
notifier = new EncryptionDecorator(notifier); // Wrap it again with the encryption decorator

notifier.send("Your order has shipped!");

// Output:
// ...also sending via SMS: Encrypted(Your order has shipped!)
// Sending core notification via Email: Encrypted(Your order has shipped!)

Interview Focus: Analysis and Trade-offs

Benefits:

  • Flexibility and OCP: This is a textbook example of the Open/Closed Principle. You can add new responsibilities (new decorators) at any time without changing existing code.
  • Dynamic Behavior: You can add or remove responsibilities from an object at runtime, which is much more flexible than the static nature of inheritance.
  • Avoids Feature Bloat: It allows you to keep the core component class clean and focused on a single responsibility, rather than bloating it with dozens of optional features.

Drawbacks:

  • Many Small Objects: The pattern can lead to a system cluttered with many small decorator classes, which can be hard to manage.
  • Complex Configuration: Decorating an object with many layers can make the initial configuration code complex.
  • Difficult to Remove a Specific Wrapper: Removing a decorator from the middle of a stack is often difficult or impossible without re-instantiating the entire chain.

How Decorator Relates to Other Concepts

The Decorator pattern is a cornerstone of flexible and extensible object-oriented design.

  • Decorator vs. Inheritance: This is the most important comparison. Use inheritance when you have a core "is-a" relationship and the behavior of the superclass must be available to the subclass. Use Decorator when you want to add optional, "stackable" behaviors to an object.
  • Comparison with Other "Wrapper" Patterns:
    • Adapter: An Adapter changes an object's interface to make it compatible with what a client expects.
    • Decorator: A Decorator enhances an object with new functionality but keeps the same interface.
    • Facade: A Facade provides a simpler interface to a complex subsystem; it doesn't typically wrap a single object, and its goal is to reduce complexity, not add functionality.
    • Proxy: A Proxy also has the same interface, but its purpose is to control access to an object (e.g., for lazy-loading or security), not to add new application behavior.
  • Real-World Examples: The most famous example is the Java I/O library, where you can wrap a base FileInputStream with decorators like BufferedInputStream (to add buffering) and GZIPInputStream (to add decompression), all of which share the same InputStream interface.