Low-level designCore7 min

Decorator

To add behavior to an object without a subclass for every combination, wrap it in an object with the same interface that does its extra work and passes the call on. Wrappers stack, so features combine at run time.

Watch it evolve1 / 8
The problem

Texts with extras

Shops want their texts signed, shortened, or both. Each combination is a subclass of SmsSender.

80
Start
Stage 1
Final

1 · The situation

Pickup texts, with extras

A small service texts customers of local shops when their orders are ready. Shops want extras: Corner Bakery wants its name signed at the end; Night Owl Deli wants long texts shortened to fit one message, and signed; a bistro wants shortening only. The first version adds each combination as a subclass of SmsSender.

The first versionJava
class SmsSender {
    String send(String to, String text) {
        return "SMS to " + to + ": " + text;
    }
}

class SignedSmsSender extends SmsSender {
    private final String store;

    SignedSmsSender(String store) {
        this.store = store;
    }

    @Override
    String send(String to, String text) {
        return super.send(to, text + " -- " + store);
    }
}

class ShortenedSmsSender extends SmsSender {
    @Override
    String send(String to, String text) {
        String cut = text.length() > 24 ? text.substring(0, 24) + "..." : text;
        return super.send(to, cut);
    }
}

class ShortenedSignedSmsSender extends SignedSmsSender {
    ShortenedSignedSmsSender(String store) {
        super(store);
    }

    @Override
    String send(String to, String text) {
        String cut = text.length() > 24 ? text.substring(0, 24) + "..." : text;
        return super.send(to, cut);
    }
}

public class Main {
    public static void main(String[] args) {
        SmsSender cornerBakery = new SignedSmsSender("Corner Bakery");
        SmsSender nightOwl = new ShortenedSignedSmsSender("Night Owl Deli");
        SmsSender bistro = new ShortenedSmsSender();
        System.out.println(cornerBakery.send("555-0101", "Your bread is ready"));
        System.out.println(nightOwl.send("555-0102", "Your sandwich order is ready for pickup"));
        System.out.println(bistro.send("555-0103", "Your table for two is confirmed for 7:30"));
    }
}
SmsSender~ send(String, String): StringSignedSmsSender− store: String~ send(String, String): StringShortenedSmsSender~ send(String, String): StringShortenedSignedSmsSender~ send(String, String): String

The class diagram of this version, drawn from its code: a subclass per combination of extras.

SMS to 555-0101: Your bread is ready -- Corner Bakery
SMS to 555-0102: Your sandwich order is r... -- Night Owl Deli
SMS to 555-0103: Your table for two is co...

2 · The change

Every new extra multiplies the subclasses

Two extras already need three subclasses, and the shortening code is written twice, once in ShortenedSmsSender and again in ShortenedSignedSmsSender, because each combination inherits from one parent only:

The same extra, written twiceJava
class ShortenedSmsSender extends SmsSender {
    @Override
    String send(String to, String text) {
        String cut = text.length() > 24 ? text.substring(0, 24) + "..." : text;
        return super.send(to, cut);
    }
}

class ShortenedSignedSmsSender extends SignedSmsSender {
    ShortenedSignedSmsSender(String store) {
        super(store);
    }

    @Override
    String send(String to, String text) {
        String cut = text.length() > 24 ? text.substring(0, 24) + "..." : text;
        return super.send(to, cut);
    }
}

Now a shop asks for a third extra, a link to track the order. With subclasses, every combination of three extras is a class of its own: seven of them. And the combinations are fixed when the code is compiled; a shop can't switch an extra on from its settings.

Subclasses, 2 extras
3
Subclasses, 3 extras
7
Wrappers, 3 extras
3

Classes needed beyond the plain sender. Subclasses need one per combination; wrappers need one per extra.

The practice checks find nothing wrong with the hierarchy: it's shallow and each class is small. The cost is in how it grows.

3 · The fix

One wrapper per extra, stacked as needed

Make each extra an object that wraps a sender, does its one thing, and passes the call to the sender inside:

MessageSender has send(to, text). SmsSender implements it and does the actual sending.

Java
class SmsSender implements MessageSender {
    public String send(String to, String text) {
        return "SMS to " + to + ": " + text;
    }
}
«interface»MessageSender+ send(String, String): StringSmsSender+ send(String, String): StringSignedSender− inner: MessageSender− store: String+ send(String, String): StringShortenedSender− inner: MessageSender− limit: int+ send(String, String): String

Each shop's sender is now built by stacking wrappers around an SmsSender:

Combinations, built at run timeJava
interface MessageSender {
    String send(String to, String text);
}

class SmsSender implements MessageSender {
    public String send(String to, String text) {
        return "SMS to " + to + ": " + text;
    }
}

class SignedSender implements MessageSender {
    private final MessageSender inner;
    private final String store;

    SignedSender(MessageSender inner, String store) {
        this.inner = inner;
        this.store = store;
    }

    public String send(String to, String text) {
        return inner.send(to, text + " -- " + store);
    }
}

class ShortenedSender implements MessageSender {
    private final MessageSender inner;
    private final int limit;

    ShortenedSender(MessageSender inner, int limit) {
        this.inner = inner;
        this.limit = limit;
    }

    public String send(String to, String text) {
        String cut = text.length() > limit ? text.substring(0, limit) + "..." : text;
        return inner.send(to, cut);
    }
}

public class Main {
    public static void main(String[] args) {
        MessageSender cornerBakery = new SignedSender(new SmsSender(), "Corner Bakery");
        MessageSender nightOwl =
                new ShortenedSender(new SignedSender(new SmsSender(), "Night Owl Deli"), 24);
        MessageSender bistro = new ShortenedSender(new SmsSender(), 24);
        System.out.println(cornerBakery.send("555-0101", "Your bread is ready"));
        System.out.println(nightOwl.send("555-0102", "Your sandwich order is ready for pickup"));
        System.out.println(bistro.send("555-0103", "Your table for two is confirmed for 7:30"));
    }
}
SMS to 555-0101: Your bread is ready -- Corner Bakery
SMS to 555-0102: Your sandwich order is r... -- Night Owl Deli
SMS to 555-0103: Your table for two is co...

The same three texts the subclasses sent.

A tracking link is now one more wrapper class, and any shop can have any combination, chosen when its sender is built, even from its settings.

4 · The pattern

Decorator: wrap, add, forward

Decorator attaches extra behavior to an object by wrapping it in another object with the same interface. The wrapper does its work, before or after the call, and forwards the call to the object inside. Because a decorator is itself a component, decorators can wrap decorators.

«interface»MessageSender+ send(String, String): StringSmsSender+ send(String, String): StringSignedSender− inner: MessageSender− store: String+ send(String, String): StringShortenedSender− inner: MessageSender− limit: int+ send(String, String): String

The roles in the texting service: one interface, the plain sender, and two decorators that hold a sender and are one.

Role

In the texting service

Its job

Component

MessageSender

The interface clients use

Concrete component

SmsSender

Does the core work

Decorator

SignedSender, ShortenedSender

Holds a component, adds behavior, forwards the call

Night Owl Deli's text passes through each layer in turn, outermost first.

Order matters. The outer wrapper acts first. Night Owl's stack shortens, then signs, so the signature survives, at a price: the signed text can run past the one-message length the shortener aimed for. Wrap the other way, signing first and shortening the result, and the signature is cut off with the rest of the long text.

5 · What it costs

Many small objects, and an order to get right

  • Order is easy to get wrong, as the signature shows, and nothing in the types prevents it.
  • Identity changes. A decorated sender isn't the original object, so code that compares identities or tests the concrete class sees the wrapper.
  • Wide interfaces are tedious. A decorator must forward every method of the interface, even the ones it doesn't change.
  • Debugging goes through layers. A stack trace passes through each wrapper.

Approach

Good when

Subclass

One fixed variation, known at compile time

Decorators

Several optional extras, combined differently per use

Flags in one class

Two or three simple options that will never grow

6 · Getting it right

Same interface, one extra each, built in one place

  • Hold the interface, not a class. A decorator that holds SmsSender can't wrap another decorator; the checks flag that as wrapping a concrete class.
  • One behavior per decorator. Keep extras independent of each other, so they combine freely.
  • Build stacks in one place, the composition root or a small factory, so the order is decided once.
  • For wide interfaces, start from a forwarding base class that passes every call through, and override only what changes. Java's FilterInputStream plays this role for the I/O streams.

Language

The usual shape

Watch out for

Java

A class implementing the interface, holding one

Forwarding every method of a wide interface

C++

A derived class holding a std::unique_ptr to the component

Ownership: each layer owns the one inside

JavaScript

An object with the same methods, or a function that wraps a function

Nothing checks that the wrapper has every method

Python

A class with the same methods, or a function wrapper

The @decorator syntax wraps functions; it's related, but not this pattern

7 · Look-alikes

Wrappers and their purposes

Several patterns wrap an object in another. What tells them apart is why.

Pattern

Same interface?

Why it wraps

Decorator

Yes

To add behavior, and stack with others

Proxy

Yes

To control access: create lazily, check permission, cache

Adapter

No

To make an object fit a different interface

Chain of Responsibility

Yes

To let each link decide whether to handle a request or pass it on

Composite

Yes

To treat a group of objects as one

Decorator and Proxy look identical in code. The difference is intent: a decorator always forwards and adds something; a proxy decides whether, when or how the call reaches the real object.

8 · In practice

Streams, and what the checks look for

Java's I/O library is built from decorators: a BufferedInputStream adds buffering to any other input stream it wraps, and streams that compress, decrypt or count bytes stack the same way.

The practice checks recognize a Decorator when a class implements an interface, is given an object of that same interface, and adds work around the same call. They warn when the held type is a concrete class. Design the parking lot: extras on a fee, such as an overnight surcharge, are natural decorators of a pricing strategy.

Check yourself

4 questions

1. A shop wants a tracking link added to its texts. What's needed with decorators?
2. Night Owl's stack is built as signing first, then shortening the signed text. What goes wrong?
3. A decorator holds an SmsSender field rather than a MessageSender. What's the problem?
4. Which is a decorator rather than a proxy?

Takeaways

Remember this

  • Decorator adds behavior by wrapping an object in another with the same interface.
  • Each decorator does one thing and forwards the call, so decorators stack.
  • It replaces a subclass per combination with a class per feature.
  • Order matters: the outermost decorator acts first.
  • A decorator holds the shared interface, never a concrete class.