โ† Back to Design Patterns
๐ŸŽ€
Structural

Decorator

Wraps an object to add new behavior dynamically, without subclassing.

The Problem

You need to add responsibilities to individual objects, not the whole class. Subclassing leads to a combinatorial explosion when features are mixed (e.g. bold + italic + underline text).

How It Works

  • โ€บEach decorator wraps the component and delegates the core call to it
  • โ€บDecorators can be stacked in any order โ€” highly composable
  • โ€บFollows Open/Closed Principle: extend behavior without modifying existing code
  • โ€บMore flexible than inheritance โ€” applied at runtime per object, not per class
  • โ€บWatch for deep nesting โ€” too many layers can be hard to debug

When to Use

โœ“Adding behavior to individual objects at runtime without affecting others
โœ“When subclassing would produce an explosion of classes for every combination
โœ“When extending with inheritance isn't possible (sealed/final classes)
โœ“Logging, validation, caching, and compression wrappers

Real-World Examples

๐ŸŽ
iOS

Combine operators (.map, .filter, .debounce chain), SwiftUI view modifiers

๐Ÿค–
Android

Compose Modifier chaining, Kotlin extension functions, OkHttp interceptors

๐ŸŒ
Web

Express middleware, React Higher-Order Components, Redux enhancers

Code Examples

Swift
protocol DataSource {
    func fetchData() -> String
}

class RemoteDataSource: DataSource {
    func fetchData() -> String { "raw data" }
}

class CachingDecorator: DataSource {
    private let wrapped: DataSource
    private var cache: String?
    init(_ ds: DataSource) { wrapped = ds }

    func fetchData() -> String {
        if let cached = cache { print("Cache hit"); return cached }
        let data = wrapped.fetchData()
        cache = data; return data
    }
}

class LoggingDecorator: DataSource {
    private let wrapped: DataSource
    init(_ ds: DataSource) { wrapped = ds }

    func fetchData() -> String {
        print("Fetching data...")
        let data = wrapped.fetchData()
        print("Got: \(data)")
        return data
    }
}

// Stack decorators โ€” order matters
let source: DataSource = LoggingDecorator(CachingDecorator(RemoteDataSource()))
_ = source.fetchData()  // "Fetching..." โ†’ cache miss โ†’ "Got: raw data"
_ = source.fetchData()  // "Fetching..." โ†’ Cache hit โ†’ "Got: raw data"