Decorator

Learning Objectives

Add behaviour to objects dynamically by wrapping them.

Stack decorators and understand ordering.

Avoid subclass explosion; know debugging and performance costs.

Prerequisites: Modules 8, 33.

Intuition and Definition

Intent: attach additional responsibilities to an object dynamically, keeping the same interface. Decorators are a flexible alternative to subclassing for extending behaviour.

Participants: Component interface, ConcreteComponent, Decorator (implements the interface and holds a wrapped component), ConcreteDecorators.

Why It Matters

Logging, retries, caching, compression, metrics and authentication are cross-cutting: they apply to many components in many combinations. Subclassing every combination explodes; decorators compose.

Syntax and Language Notes

# Python function decorators implement the same idea for functions
import functools
def retry(times):
    def wrap(f):
        @functools.wraps(f)
        def inner(*a, **kw):
            for i in range(times):
                try: return f(*a, **kw)
                except IOError:
                    if i == times - 1: raise
        return inner
    return wrap

Complete Example

#include <iostream>
#include <memory>
#include <string>

struct DataSource {                                          // Component
    virtual ~DataSource() = default;
    virtual std::string read() = 0;
};
struct VinReader : DataSource {                              // ConcreteComponent (flaky)
    int calls = 0;
    std::string read() override {
        if (++calls < 3) throw std::runtime_error("bus timeout");
        return "WVWZZZ1KZ6W123456";
    }
};

class Decorator : public DataSource {                        // base decorator
protected:
    std::unique_ptr<DataSource> inner_;
public:
    explicit Decorator(std::unique_ptr<DataSource> d) : inner_(std::move(d)) {}
};
class Retry : public Decorator {
    int attempts_;
public:
    Retry(std::unique_ptr<DataSource> d, int n) : Decorator(std::move(d)), attempts_(n) {}
    std::string read() override {
        for (int i = 1;; ++i) {
            try { return inner_->read(); }
            catch (const std::exception& e) {
                std::cout << "  retry: attempt " << i << " failed (" << e.what() << ")\n";
                if (i == attempts_) throw;
            }
        }
    }
};
class Cache : public Decorator {
    std::st
…
cache: miss
  retry: attempt 1 failed (bus timeout)
  retry: attempt 2 failed (bus timeout)
first read:  WVWZZZ1KZ6W******
  cache: hit
second read: WVWZZZ1KZ6W******

Step-by-Step Walkthrough

The call goes Mask → Cache → Retry → VinReader. The reader fails twice; Retry absorbs both failures; the third call succeeds.

Cache stores the unmasked VIN; Mask hides the serial number on the way out.

The second read is served from the cache: no reader call, no retries.

Reordering changes behaviour: Cache(Mask(...)) would cache the masked value; Retry(Cache(...)) would retry the cache.

classDiagram
  class DataSource {
    <<interface>>
    +read() string
  }
  DataSource <|.. VinReader
  DataSource <|.. Decorator
  Decorator o-- DataSource : inner
  Decorator <|-- Retry
  Decorator <|-- Cache
  Decorator <|-- Mask

Common Mistakes

Order bugs — e.g. caching before authorization exposes data; document the intended stack.

Identity checks — a decorated object isn't == the inner one; dynamic_cast to the concrete type fails.

Too many tiny layers — hard to debug (deep stack traces) and each layer adds a virtual call.

Decorators that change the contract (e.g. swallow exceptions the interface promises to throw).

Trade-offs and Alternatives

Decorator adds behaviour without subclass explosion and allows run-time composition. Alternatives: middleware pipelines (a list of handlers — Chain of Responsibility, Module 45), aspect-oriented programming, or compile-time mixins/CRTP when the combination is fixed.

Real-World Use

Java I/O streams (BufferedInputStream(new FileInputStream(...))), .NET Stream wrappers (GZipStream, CryptoStream), HTTP client handlers (DelegatingHandler), Python @decorators, logging/metrics wrappers around repositories.

Practice

Q1. (Easy) Does a decorator change the interface?

Answer: No — it implements the same interface it wraps.

Q2. (Medium) Why does the order of decorators matter? Give an example.

Answer: Each wraps the next: Cache(Mask(src)) caches masked data, Mask(Cache(src)) caches raw data and masks on every read; Retry(Cache(src)) would retry around the cache instead of the source.

Q3. (Advanced) Write a Timing decorator that prints how many calls it has forwarded (deterministic stand-in for timing).

#include <iostream>
#include <memory>
#include <string>
struct Source { virtual ~Source() = default; virtual std::string read() = 0; };
struct Fixed : Source { std::string read() override { return "42"; } };
class Counting : public Source {
    std::unique_ptr<Source> inner_; int calls_ = 0;
public:
    explicit Counting(std::unique_ptr<Source> s) : inner_(std::move(s)) {}
    std::string read() override { ++calls_; return inner_->read(); }
    int calls() const { return calls_; }
};
int main() {
    Counting c(std::make_unique<Fixed>());
    c.read(); c.read(); c.read();
    std::cout << "forwarded " << c.calls() << " calls, last value " << c.read() << "\n";
}
forwarded 3 calls, last value 42

Interview Question

Q1. "Decorator vs inheritance for adding logging and caching to a repository?"

Answer: Inheritance would need LoggingRepo, CachingRepo, LoggingCachingRepo, … for every combination and every repository implementation, chosen at compile time. Decorators implement the repository interface and wrap any repository, so combinations are assembled at run time (Logging(Caching(SqlRepo))), each concern is tested in isolation, and the core repository stays unchanged (OCP). The cost is indirection and attention to ordering.

Quick Quiz

Q1. What does a decorator hold?

Answer: A reference to the wrapped component (same interface).

Q2. Name a standard-library decorator.

Answer: Java's BufferedInputStream, .NET's GZipStream, etc.

Q3. Main debugging cost?

Answer: Deep wrapper chains and stack traces; behaviour depends on order.

Summary

Decorators wrap objects to add behaviour while keeping the interface; they compose at run time.

Order matters; keep contracts intact.

Related: Modules 33, 39, 45.