Greetings, traveler!
We continue researching Structural Design Patterns as part of our Design Patterns course. It’s the turn of the Decorator Design Pattern. We can use this pattern to modify an object’s behavior without inheritance. A decorator is a wrapper that alters an object’s behavior to provide the desired result in a specific situation. The decorator has a similar interface to the object it wraps, which means it can be used as a wrapper for other decorators. However, it’s important not to overdo it with decorators, as this can make the code more complicated and challenging to understand.
Key Roles (Participants)
When you apply the Decorator pattern, these are the conceptual roles involved:
- Component — defines the common interface for both the core object and its decorators.
- ConcreteComponent — a default, basic implementation of the Component.
- Decorator — also implements the Component interface and holds a reference to another Component (which may be either a ConcreteComponent or another Decorator).
- ConcreteDecorator — extends or modifies behavior of the wrapped component. You can have multiple of these, and they can be combined (nested) arbitrarily.
Basic Example: Burgers and Extra Cheese (Swift)
Let’s start with a minimal example to illustrate the pattern. Imagine you have a burger object. You want to optionally add “extra cheese”, but without creating a new subclass for every variation.
protocol Burger {
var name: String { get }
var price: Double { get }
}
struct BasicBurger: Burger {
let name: String = "Cheeseburger"
let price: Double = 5.00
}Now we define a decorator:
struct ExtraCheeseDecorator: Burger {
private let burger: Burger
private let extraCheeseSlices: Int
var name: String {
burger.name + (extraCheeseSlices > 0 ? " + \(extraCheeseSlices) extra cheese" : "")
}
var price: Double {
burger.price + Double(extraCheeseSlices) * 0.30
}
init(burger: Burger, extraCheeseSlices: Int) {
self.burger = burger
self.extraCheeseSlices = max(0, extraCheeseSlices)
}
}Usage:
let base = BasicBurger()
let cheeseBurger = ExtraCheeseDecorator(burger: base, extraCheeseSlices: 2)
print(cheeseBurger.name) // "Cheeseburger + 2 extra cheese"
print(cheeseBurger.price) // 5.60This setup already captures the essence: the decorator conforms to Burger, wraps another burger, and extends its behavior (price and name) without subclassing.
Chaining Decorators: Composition in Action
One of the core powers of the Decorator pattern is composability — you can wrap a decorated object again to layer behavior. For example, you might want to add bacon on top of a cheeseburger with extra cheese:
struct BaconDecorator: Burger {
private let burger: Burger
var name: String {
burger.name + " + bacon"
}
var price: Double {
burger.price + 1.00
}
init(burger: Burger) {
self.burger = burger
}
}
// Chaining decorators
let base = BasicBurger()
let withCheese = ExtraCheeseDecorator(burger: base, extraCheeseSlices: 2)
let deluxeBurger = BaconDecorator(burger: withCheese)
print(deluxeBurger.name) // "Cheeseburger + 2 extra cheese + bacon"
print(deluxeBurger.price) // 6.60Such layering makes the Decorator pattern far more powerful than simply flag-based configurations. Each decorator remains small, focused, and testable.
When to Use the Decorator Pattern
Decorator shines in scenarios satisfying these conditions:
- You need to extend or modify behavior dynamically at runtime, possibly varying between instances.
- You want to avoid subclass explosion: many variants produced by combinations of behaviors.
- You want to mix and match behaviors (e.g., logging + caching + retry) without hard-coding combinations.
- Each added behavior is orthogonal and can be expressed as a separate concern.
- You want to preserve a single, simple public interface despite internal complexity.
If you only need a small number of variations and can determine them at compile time, a builder or initializer with configuration options may suffice — Decorator becomes more valuable when the number and combination of behaviors grows.
Real-World Use Cases in Swift
Here are some realistic scenarios where Decorator delivers substantial benefit:
Network Clients with Logging, Retry, and Metrics
- Base Component:
NetworkClient, with methodfunc request(...) -> Data. - Decorators:
- LoggingDecorator (logs requests/responses),
- RetryDecorator (retries on failure),
- MetricsDecorator (records request timing, success/failure, payload sizes).
- Compose them freely: e.g.,
Retry → Metrics → Logging → BaseClient.
This allows decorating any client instance differently — for example, test builds with extra logging or production builds with metrics only.
Storage Layer Wrappers
- Base Component: a simple key-value storage (e.g., wrapping
UserDefaultsor file storage). - Decorators:
- EncryptionDecorator (encrypts / decrypts data),
- CacheDecorator (keeps in-memory cache),
- LoggingDecorator (records read/write operations).
This design keeps concerns separated and composable instead of creating a dense tower of subclasses.
UI Data Formatters / Decorators
- Base Component: a data formatter, e.g.
PriceFormatterwithfunc format(price: Double) -> String. - Decorators:
- CurrencySymbolDecorator (adds currency symbol),
- DiscountDecorator (applies discounts),
- LocalizationDecorator (applies locale-specific formatting).
You can chain decorators depending on context — e.g. for a sale UI vs normal pricing.
Advantages and Drawbacks
Strengths
- Flexibility & extensibility. Easily add behaviors without altering existing classes.
- Avoids inheritance explosion. No need to define a separate subclass for every variation.
- Runtime configuration. Behaviors can vary per-instance and be combined dynamically.
- Separation of concerns / single responsibility. Each decorator encapsulates one concern (e.g., logging, caching, encryption).
- Composition over inheritance. In Swift, protocols + composition often feel cleaner and safer than deep class hierarchies.
Trade-offs and Pitfalls
- Increased complexity. Multiple layers of decorators may make it harder to follow the flow or debug.
- Overuse risk. If you wrap too many concerns, the code can become obfuscated and difficult to reason about.
- Performance overhead. Each decorator adds an extra indirection — trivial for UI or network code, but could matter in tight loops.
Recommendations
- Use protocols for the base interface, not concrete classes; this maximizes flexibility.
- Prefer composition (struct or final class) over inheritance.
- Keep each decorator focused on a single responsibility — do not bundle unrelated behaviors.
- Provide comprehensive tests for typical decorator chains, especially in complex domains like networking or storage.
- Document intended decorator usage, order (if important), and constraints (e.g., encryption before caching).
Conclusion
The decorator is a useful tool but should be used carefully and with knowledge to avoid creating overly complex code.
Alight then! Now that we have finished with the Decorator, we can move on to the next Design Pattern, the Facade. See you in the following article.
Check out other posts in the Design Patterns series:
- Visitor Design Pattern in Swift
- Template Method Design Pattern in Swift
- Strategy Design Pattern in Swift
- State Design Pattern in Swift
- Observer Design Pattern in Swift
- Memento Design Pattern in Swift
- Mediator Design Pattern in Swift
- Iterator Design Pattern in Swift
- Command Design Pattern in Swift
- Chain of Responsibility Design Pattern in Swift
- Proxy Design Pattern in Swift
- FlyWeight Design Pattern in Swift
- Facade Design Pattern in Swift
- Composite Design Pattern in Swift
- Bridge Design Pattern in Swift
- Adapter Design Pattern in Swift
- Singleton Design Pattern in Swift
- Prototype Design Pattern in Swift
- Builder Design Pattern in Swift
- Abstract Factory Design Pattern in Swift
- Factory Method Design Pattern in Swift
- Design Patterns: Basics
