โ 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"