Skip to main content

Decorator

complexitypopularity

Attach extra behavior to an object by wrapping it in another object that implements the same interface, stackable as many layers deep as you like.

The problem

You maintain a notification library. Version one had a Notifier class that emailed a list of addresses. Then users wanted SMS for critical issues, and Slack for corporate accounts, and Facebook for reasons of their own. Fine: one subclass per channel.

Then someone asks the obvious question. "If the building is on fire, can I have all of them?" Now you are writing SMSAndSlackNotifier, and SMSAndSlackAndFacebookNotifier, and the library is bloating faster than the client code that uses it.

The solution

The trouble is inheritance itself. It is static - you cannot change an object's class after construction - and in most languages a class gets exactly one parent, so combinations must become classes.

Composition escapes both. Build a wrapper: an object implementing the same interface as its target, holding a reference to it, forwarding every call and doing something extra before or after. Because the wrapper's field accepts anything satisfying the interface, a wrapper can hold another wrapper. Now behaviors are Lego bricks, snapped together at runtime in whatever combination the configuration file asked for.

SalaryManagerEncryptionDecoratorCompressionDecoratorFileDataSourcewriteData(records)1encrypt(records)2writeData(cipher)3compress(cipher)4writeData(blob)5ok6ok7ok8
  1. 1The client holds one DataSource reference and has no idea it is three objects deep.
  2. 2The outermost decorator does its work first, before delegating. Order in the stack is order of execution.
  3. 3It passes the transformed payload down through the same interface it was called with.
  4. 4The next layer adds its own contribution, oblivious to the layer above it.
  5. 5The bottom of the stack is the real component. It writes bytes and asks no questions.
  6. 6Control unwinds back through the stack. On read(), each layer would undo its work here instead.
  7. 7Every layer keeps the contract intact, so nobody upstream notices the detour.
  8. 8The client got a plain writeData() call, and the file on disk is compressed and encrypted.

Structure

The component interface is shared by everyone. The concrete component provides the base behavior. The base decorator stores a component and delegates blindly, existing mostly so that concrete decorators can be short. Concrete decorators override methods, run their own logic, and always call through.

«interface»DataSourcewriteData(data)readData()COMPONENTFileDataSourcefilenamewriteData(data)readData()CONCRETEDataSourceDecoratorwrappee: DataSourcewriteData(data)readData()BASEEncryptionDecoratorwriteData(data)readData()CONCRETECompressionDecoratorwriteData(data)readData()CONCRETE
implementsextends

Code

Compression and encryption layered over a file, chosen at runtime.

// One notifier class per channel, then one per combination.
class Notifier is
method send(message) is
// email the list
 
class SMSNotifier extends Notifier is
// ...
class SlackNotifier extends Notifier is
// ...
class FacebookNotifier extends Notifier is
// ...
 
// "Can I get SMS and Slack together?" Sure, one moment:
class SMSAndSlackNotifier extends Notifier is
// ...
class SMSAndFacebookNotifier extends Notifier is
// ...
class SMSAndSlackAndFacebookNotifier extends Notifier is
// ...
 
// Four channels is fifteen combinations. Five is thirty-one.
// And the choice is frozen at compile time either way.
// One interface, one real implementation, and optional layers.
interface DataSource is
method writeData(data)
method readData():data
 
class DataSourceDecorator implements DataSource is
protected field wrappee: DataSource // component type: stacking allowed
 
method writeData(data) is
wrappee.writeData(data)
 
// Behavior is chosen at runtime, by configuration, not by class name:
source = new FileDataSource("salary.dat")
if (enabledEncryption) source = new EncryptionDecorator(source)
if (enabledCompression) source = new CompressionDecorator(source)
 
new SalaryManager(source).save()
// Three optional layers cover eight behaviors with three classes.
// The component interface: the operations wrappers may alter.
interface DataSource is
method writeData(data)
method readData():data
 
// The concrete component does the base job.
class FileDataSource implements DataSource is
constructor FileDataSource(filename) { ... }
 
method writeData(data) is
// Write the bytes to the file.
 
method readData():data is
// Read the bytes back from the file.
 
// The base decorator implements the same interface and holds a
// component. Declaring the field as DataSource (not FileDataSource)
// is what lets a wrapper wrap another wrapper.
class DataSourceDecorator implements DataSource is
protected field wrappee: DataSource
 
constructor DataSourceDecorator(source: DataSource) is
wrappee = source
 
// By default it is a pure pass-through. Subclasses add the spice.
method writeData(data) is
wrappee.writeData(data)
 
method readData():data is
return wrappee.readData()
 
// Concrete decorators act before or after the delegated call, but
// they always make the call.
class EncryptionDecorator extends DataSourceDecorator is
method writeData(data) is
// 1. Encrypt the incoming data.
// 2. Hand the ciphertext to wrappee.writeData().
 
method readData():data is
// 1. Pull data up from wrappee.readData().
// 2. Decrypt it.
// 3. Return the plaintext.
 
class CompressionDecorator extends DataSourceDecorator is
method writeData(data) is
// 1. Compress the incoming data.
// 2. Hand the blob to wrappee.writeData().
 
method readData():data is
// 1. Pull data up from wrappee.readData().
// 2. Decompress it.
// 3. Return the original bytes.
 
// Business code takes a pre-built DataSource and never asks how many
// layers came with it.
class SalaryManager is
field source: DataSource
 
constructor SalaryManager(source: DataSource) { ... }
 
method load() is
return source.readData()
 
method save() is
source.writeData(salaryRecords)
 
// The stack is assembled at runtime, from configuration.
class ApplicationConfigurator is
method configurationExample() is
source = new FileDataSource("salary.dat")
if (enabledEncryption)
source = new EncryptionDecorator(source)
if (enabledCompression)
source = new CompressionDecorator(source)
 
manager = new SalaryManager(source)
salary = manager.load()

When to use it

  • You need to assign extra responsibilities to objects at runtime without disturbing the code that uses them - logging, caching, retries, rate limiting, encryption, all layered over the same call.
  • Inheritance is awkward or unavailable. A final class cannot be subclassed, but it can certainly be wrapped.
  • One monolithic class implements many optional behaviors and wants to be split into small ones you can mix.

Pitfalls

  • Order is semantics. Compress-then-encrypt produces a different file from encrypt-then-compress, and only one of them compresses well. Write the intended order down.
  • Extraction is hard. Removing the third wrapper out of five means rebuilding the stack. If you need that regularly, you want a list of behaviors, not a chain of objects.
  • Identity vanishes. Once wrapped, instanceof checks, equality and debugger output all talk about the wrapper. Stack traces become a tower of writeData frames.
  • Ugly assembly code. The configuration block that builds the stack is rarely pretty. Hide it behind a factory or a builder rather than repeating it at every call site.

Don't confuse it with

  • Adapter. An adapter deliberately changes the interface so foreign code can be called; a decorator preserves it and enhances what happens behind it. That symmetry is precisely why decorators nest and adapters generally do not.
  • Proxy. Nearly the same structure, entirely different reason. A proxy is about control - lazy loading, permissions, caching - and typically manages the service object's whole lifecycle itself. Decorator stacks are assembled by the client on purpose.
  • Composite. A decorator is a composite that happens to have one child. A composite aggregates results from many children; a decorator embellishes the one it has. Nothing stops you decorating a node inside a composite tree.
  • Strategy. Decorator changes an object's skin from the outside; Strategy swaps its guts. If the variation is "which algorithm", reach for Strategy. If it is "what happens around the call", reach for Decorator.
  • Chain of Responsibility. Both pass a call down a series of objects. A CoR handler is allowed to stop the chain dead; a decorator is not. Break the flow and you have changed patterns without meaning to.

Check yourself

Question 1 of 5

Why is the wrapped field typed as the component interface rather than the concrete component class?