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.
Texts with extras
Shops want their texts signed, shortened, or both. Each combination is a subclass of SmsSender.
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.
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"));
}
}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:
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.
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.
class SmsSender implements MessageSender {
public String send(String to, String text) {
return "SMS to " + to + ": " + text;
}
}Each shop's sender is now built by stacking wrappers around an SmsSender:
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.
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 |
| The interface clients use |
Concrete component |
| Does the core work |
Decorator |
| 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
SmsSendercan'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
FilterInputStreamplays 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 | 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 |
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
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.