Explain the Decorator Pattern in iOS
Sources & Resources
Main Source: 🔗 Adding Functionality to Legacy Code Without Modifying It | Islam Moussa
Further Reading:
The Decorator Pattern allows you to add functionality to an object dynamically without modifying its code. It uses composition to wrap an object with additional behavior.
Concept Overview​
The Decorator Pattern is a structural design pattern that dynamically modifies or enhances the behavior of an object. Instead of extending a class with inheritance, the pattern relies on wrapping the object and delegating its behavior to add new functionality.
Key components:
- Component Protocol: Declares the interface for objects that can be dynamically modified.
- Concrete Component: The class implementing the component protocol (the object to be enhanced).
- Decorator Class: Implements the component protocol and wraps the concrete component to add behavior dynamically.
Example in iOS: Adding Functionality to Legacy Code​
Here’s how the Decorator Pattern can enhance a legacy class without altering its original code.
Legacy Class​
class LegacyClass {
func doSomething() -> String {
return "Legacy functionality"
}
}
Component Protocol​
protocol LegacyProtocol {
func doSomething() -> String
}
extension LegacyClass: LegacyProtocol {}
Decorator Class​
class Decorator: LegacyProtocol {
private let wrapped: LegacyProtocol
private let additionalFunctionality: () -> String
init(wrapped: LegacyProtocol, additionalFunctionality: @escaping () -> String) {
self.wrapped = wrapped
self.additionalFunctionality = additionalFunctionality
}
func doSomething() -> String {
let legacyResult = wrapped.doSomething()
let additionalResult = additionalFunctionality()
return "\(legacyResult) with \(additionalResult)"
}
}
Usage​
let legacyInstance: LegacyProtocol = LegacyClass()
let decoratorInstance = Decorator(wrapped: legacyInstance, additionalFunctionality: { "new functionality" })
print(legacyInstance.doSomething()) // Output: "Legacy functionality"
print(decoratorInstance.doSomething()) // Output: "Legacy functionality with new functionality"
How It Works​
- Component Protocol: The
LegacyProtocol
ensures compatibility between the legacy class and the decorator. - Concrete Component: The
LegacyClass
provides the original functionality. - Decorator: The
Decorator
wrapsLegacyProtocol
and dynamically adds new functionality.
Unit Testing​
The Decorator Pattern is easily testable, ensuring that new functionality works as expected without impacting the original behavior.
import XCTest
class DecoratorTests: XCTestCase {
func testDoSomething() {
// Given
let legacyInstance: LegacyProtocol = LegacyClass()
let decoratorInstance = Decorator(wrapped: legacyInstance, additionalFunctionality: { "new functionality" })
// When
let result = decoratorInstance.doSomething()
// Then
XCTAssertEqual(result, "Legacy functionality with new functionality")
}
}
Benefits of the Decorator Pattern​
- Extensibility: Add functionality without altering existing code.
- Open/Closed Principle: Supports open for extension and closed for modification.
- Flexibility: Dynamically compose behaviors without inheritance.
When to Use​
- When extending functionality without subclassing.
- When working with legacy code or third-party libraries.
- To avoid a rigid class hierarchy.
Limitations​
- Complexity: Can lead to a system of many small objects.
- Debugging: Wrapped objects can make debugging and tracing behavior challenging.
- The Decorator Pattern enhances objects dynamically without altering their code.
- It involves components, concrete implementations, and decorators.
- Useful for adding new features to legacy code or third-party libraries without breaking existing functionality.