Facebook Pixel

Decorator Pattern

The subclass explosion problem

You start with a DataStream that writes bytes to a file. Then someone needs encrypted writes. You subclass: EncryptedDataStream. Then someone needs compression. Another subclass: CompressedDataStream. Supporting both at once forces a third subclass, CompressedEncryptedDataStream. Add logging and you have eight combinations for three features. Every new cross-cutting concern doubles the count.

This is the smell the Decorator pattern fixes. Instead of baking optional features into the inheritance hierarchy, you wrap objects at runtime. Each feature lives in its own thin wrapper that delegates to whatever is inside it — another wrapper, or the real object.

How it works

The pattern rests on a single interface that both the real object and every wrapper implement. Because every layer speaks the same contract, a caller cannot tell whether it is talking to the raw object or to ten layers of wrapping. You add features by composing wrappers, not by multiplying subclasses.


The DataStreamDecorator base holds the wrapped instance and forwards calls to it. Subclasses override only what they change — encryption before and after the delegate call, compression likewise. Three features remain three classes regardless of how many combinations a caller needs.

A write call through two stacked decorators shows the layered delegation in action.

Bad: subclass per combination

The version that triggers the explosion gives each combination its own class. Adding a fourth feature requires touching every existing combination.

1# Bad: three features produce eight subclasses; adding a fourth needs sixteen.
2class DataStream:
3    def write(self, data: bytes) -> None: ...
4    def read(self) -> bytes: ...
5
6class FileDataStream(DataStream): ...
7class EncryptedDataStream(DataStream): ...
8class CompressedDataStream(DataStream): ...
9class LoggedDataStream(DataStream): ...
10class CompressedEncryptedDataStream(DataStream): ...
11class LoggedEncryptedDataStream(DataStream): ...
12class LoggedCompressedDataStream(DataStream): ...
13class LoggedCompressedEncryptedDataStream(DataStream): ...
14# Every new cross-cutting concern doubles this list.

Good: wrappers composed at runtime

The Decorator version keeps three classes — one per feature — and composes them at the call site. The caller decides which combination it needs; the classes themselves know nothing about each other.

1from abc import ABC, abstractmethod
2
3class DataStream(ABC):
4    @abstractmethod
5    def write(self, data: bytes) -> None: ...
6    @abstractmethod
7    def read(self) -> bytes: ...
8
9
10class FileDataStream(DataStream):
11    def __init__(self, path: str) -> None:
12        self._path = path
13        self._buf = b""
14
15    def write(self, data: bytes) -> None:
16        self._buf = data  # simplified; real impl writes to disk
17
18    def read(self) -> bytes:
19        return self._buf
20
21
22class DataStreamDecorator(DataStream, ABC):
23    def __init__(self, wrapped: DataStream) -> None:
24        self._wrapped = wrapped
25
26    def write(self, data: bytes) -> None:
27        self._wrapped.write(data)
28
29    def read(self) -> bytes:
30        return self._wrapped.read()
31
32
33class EncryptionDecorator(DataStreamDecorator):
34    def write(self, data: bytes) -> None:
35        encrypted = bytes(b ^ 0xFF for b in data)  # XOR placeholder
36        self._wrapped.write(encrypted)
37
38    def read(self) -> bytes:
39        return bytes(b ^ 0xFF for b in self._wrapped.read())
40
41
42class CompressionDecorator(DataStreamDecorator):
43    def write(self, data: bytes) -> None:
44        import zlib
45        self._wrapped.write(zlib.compress(data))
46
47    def read(self) -> bytes:
48        import zlib
49        return zlib.decompress(self._wrapped.read())
50
51
52# Compose at the call site — no new class needed.
53stream = CompressionDecorator(EncryptionDecorator(FileDataStream("/tmp/out")))
54stream.write(b"order:123:confirmed")

Wrapping order matters

The order in which decorators are stacked determines the behavior, and different orders are not interchangeable. Consider the write path with compression and encryption. If you wrap compression inside encryption — EncryptionDecorator(CompressionDecorator(file)) — data is first compressed, then the compressed bytes are encrypted and written. Reading reverses both steps in order: decrypt, then decompress. If instead you wrap encryption inside compression — CompressionDecorator(EncryptionDecorator(file)) — data is first encrypted, then the ciphertext is compressed. Because encrypted data is close to random, a general-purpose compressor gains almost nothing and may even expand it. The resulting files from these two orderings are structurally different; a reader configured for one order cannot decode output produced by the other.

Choosing the order is a design decision, not a detail. Compress-then-encrypt is the standard approach in protocols such as TLS, because compressing before encryption preserves the redundancy patterns that compressors exploit. Document the intended stack order wherever you construct the chain so that the reader and writer always agree.

When NOT to use this

Use the Decorator pattern when the set of features is open-ended and their combinations are unpredictable. If you know at compile time that you will always have exactly two configurations — say, a plain connection and a TLS-wrapped one — two subclasses are simpler and easier to follow. Decorators add indirection; tracing a bug through four layers of wrapping in a debugger is genuinely painful.

Avoid it also when the interface is large. If DataStream exposed twenty methods, every decorator would need to forward all twenty even if it cares about only one. That boilerplate is the signal to look for a different approach — composition through a field rather than the wrapping contract, or narrowing the interface so decorators stay lean.

Invest in Yourself
Your new job is waiting. 83% of people that complete the program get a job offer. Unlock unlimited access to all content and features.
Go Pro