You have a FileStream that writes bytes. You would also like the option of
compressing them, encrypting them, and batching the writes.
What people write first
A subclass per combination.
class FileStream { write(data: string) {} }
class GzipFileStream extends FileStream {}class EncryptedFileStream extends FileStream {}class GzipEncryptedFileStream extends FileStream {}class BufferedGzipEncryptedFileStream extends FileStream {}// …Three optional behaviours is eight classes. Four is sixteen. And the ordering matters — compressing then encrypting is not the same as encrypting then compressing — so it is really permutations, not combinations.
This is the class explosion, and it is the specific problem Decorator exists to solve.
The pattern
A decorator implements the same interface as the thing it wraps, and holds one of them. Those two properties together are what make it composable.
interface Stream { write(data: string): void;}
class FileStream implements Stream { write(data: string) { /* … */ }}
class GzipStream implements Stream { constructor(private readonly inner: Stream) {} // ← holds a Stream
write(data: string) { this.inner.write(gzip(data)); // ← is a Stream }}
class EncryptedStream implements Stream { constructor(private readonly inner: Stream) {} write(data: string) { this.inner.write(encrypt(data)); }}Now the combinations are built at runtime, and there are no new classes:
const plain = new FileStream();const secure = new EncryptedStream(new FileStream());const both = new EncryptedStream(new GzipStream(new FileStream()));Toggle the layers and watch the composition change:
enc(gz(payload))
2 wrappers, each holding the one beneath it.
Order is a decision, not a detail
new EncryptedStream(new GzipStream(file)) compresses first, then encrypts.
Reverse them and you compress ciphertext.
That is not a stylistic preference — encrypted data is statistically random, and random data does not compress. Get the order backwards and your archive is the same size as the original, with no error to tell you.
In this language, it is often a function
Same observation as Strategy: the class is scaffolding around an interface with one method.
type Write = (data: string) => void;
const withGzip = (inner: Write): Write => (data) => inner(gzip(data));const withEncryption = (inner: Write): Write => (data) => inner(encrypt(data));
const write = withEncryption(withGzip(writeToFile));This is the same idea as an HTTP middleware chain, a higher-order React
component, or a Python @decorator — all of which are Decorator with the
ceremony removed.
What it costs
Debugging depth. A stack trace through five decorators is five nearly identical frames, and finding which layer misbehaved means reading all of them.
Identity is lost. wrapped instanceof FileStream is false. Anything doing
type checks, reference equality, or reflection on the concrete class will not
see through the wrapper.
No random access to the middle. You cannot ask a stack “is encryption enabled?” without walking it or tracking that separately.
Decorator, Proxy, Adapter
All three wrap an object and forward calls. The difference is intent, and it is worth being able to say which one you are writing:
| Interface | Purpose | |
|---|---|---|
| Decorator | same as the wrapped object | add behaviour, stackably |
| Proxy | same as the wrapped object | control access — lazy loading, caching, permissions |
| Adapter | different from the wrapped object | make an incompatible thing fit |
Decorator and Proxy are structurally identical; only the reason differs. Adapter is distinguishable from the outside, because the interface changes.