Categoría: Dessing Patterns

  • Decorator pattern in Swift

    Decorator pattern in Swift

    This post aims to give you a clear, intuitive perspective on the Decorator pattern. As a software developer, you have likely implemented this pattern countless times without realizing it.

    Its main advantage lies in its flexibility: because a decorator acts as an independent add-on, you can seamlessly attach or remove capabilities without disrupting the core system.

    The Decorator pattern

    Here is a refined, clear, and professional rewrite of the explanation of the Decorator Pattern, tailored for Swift developers:

    The core idea of this pattern is to dynamically wrap or stack behavior around a central component. Crucially, wrapping an object with layers of new functionality must never alter or break its core identity or fundamental behavior.

    Everyday Analogies:

    • Clothing: A person wearing a t-shirt is still a person. Adding a sweater adds a layer of protection, and putting a coat over that adds even more, but at their core, they remain the same person.

    • In astronomy: the Decorator pattern is best visualized as a Rocky Planet Core (the Decoratee) wrapped by concentric cosmic layers, such as an Atmosphere or a Ring System (the Decorators). Just as adding an atmosphere provides radiation filtering or adding rings reflects light, each outer layer enhances the primary celestial body with new features without altering its fundamental identity at the center

    • Coffee: Plain coffee becomes a café au lait when you add milk, and a cappuccino when you sprinkle cocoa powder on top—yet at its core, it remains coffee.

    Implementation in Swift:

    To preserve this core behavior in code, the Decorator pattern relies on two fundamental Swift principles:

    • Conformance to the Same Protocol: Both the core object (decoratee) and the wrapper (decorator) must conform to the exact same protocol. This ensures they are fully interchangeable to any client code.

    • Dependency Injection: The decorator receives the decoratee instance (typically via initializer injection) and delegates work to it. Any client interacting with the wrapper is seamlessly interacting with the underlying object, augmented by the new behavior.

    When we turn into Swift:

    protocol Component {
        func execute() -> String
    }
    
    struct Decoratee: Component {
        func execute() -> String {
            return "Base execution"
        }
    }
    
    class Decorator: Component {
        private let component: Component
    
        init(component: Component) {
            self.component = component
        }
    
        func execute() -> String {
            return component.execute()
        }
    }

    The compositiona and usage will be following:

    let coreObject : Component = Decoratee()
    let decoratedObject : Component = Decorator(component: coreObject)
    let result = decoratedObject.execute()
    
    print(result)

    Roles

    Despite its simplicity, the Decorator pattern typically manifests in four distinct functional roles based on how it interacts with the inputs and outputs of the decoratee:

    • Monitor: The decorator observes execution without modifying any input or output data (e.g., Loggers, Profilers, or Analytics trackers).

    • Guard / Interceptor: The decorator intercepts and modifies, validates, or blocks the input before it reaches the decoratee (e.g., Cache lookups, Rate limiters, DDoS filters, or Failed login counters).

    • Fallback / Output Formatter: The decorator transforms or supplies the output after the decoratee executes, leaving inputs untouched (e.g.   Currency formatters, or Response mappers).

    • Transformer: The decorator actively alters both the input and output data (e.g., Encryption layers, where incoming data is encrypted and outgoing data is decrypted).

    Across all four roles, the underlying principle remains unchanged: the core identity and essential business logic of the decoratee are never altered—even during data transformation, where the underlying information remains the same despite its changed representation.

     

    Monitor

    In this role no transformation is performed, is the most inoquoos, where you do not notice any behaviour but at the end is there, we can add a decorator for profiling any heavy task, but just adding on debug mode but not on release. 

    import Foundation
    
    // Interface & Decoratee
    protocol DataService {
        func fetch(query: String) async throws -> String
    }
    
    struct RemoteService: DataService {
        func fetch(query: String) async throws -> String {
            try await Task.sleep(for: .milliseconds(600))
            return "Data for '\(query)'"
        }
    }
    
    // Performance Monitor Decorator
    struct PerformanceMonitor: DataService {
        let wrapped: DataService
        var threshold: Duration = .milliseconds(500)
    
        func fetch(query: String) async throws -> String {
            let clock = ContinuousClock()
            let elapsed = try await clock.measure { _ = try await wrapped.fetch(query: query) }
            
            let ms = elapsed.components.seconds * 1000 + Int64(elapsed.components.attoseconds / 1_000_000_000_000_000)
            print("⏱️ Execution took \(ms)ms \(elapsed > threshold ? "⚠️ [THRESHOLD EXCEEDED]" : "")")
            
            return try await wrapped.fetch(query: query)
        }
    }

    By using a Factory pattern, you can seamlessly configure a decorator—such as injecting a logger or profiler—whenever the debug flag is enabled.

    enum ServiceFactory {
        static func makeDataService(isDebug: Bool) -> DataService {
            let baseService = RemoteService()
    
            if isDebug {
                return PerformanceMonitor(wrapped: baseService)
            }
            return baseService
        }
    }
    
    let isDebugMode = true 
    let serviceA: DataService = ServiceFactory.makeDataService(isDebug: isDebugMode)

    Guard / Interceptor

    This decorator acts as a guard / interceptor by evaluating incoming inputs and triggering a specific action—such as returning cached data—whenever a condition is met. The core behavior remains intact: the caller receives the requested data, completely agnostic of whether it originated from the cache or the underlying service.

    Here is an example:

    import Foundation
    
    // 1. Component
    protocol DataService {
        func fetchData(key: String) -> String
    }
    
    // 2. Decoratee (Core network fetch)
    struct NetworkDataService: DataService {
        func fetchData(key: String) -> String {
            print("🌐 Fetching from network...")
            return "Data for \(key)"
        }
    }
    
    // 3. Decorator (Caching layer)
    final class CacheDecorator: DataService {
        private let decoratee: DataService
        private var cache: [String: String] = [:]
    
        init(decoratee: DataService) {
            self.decoratee = decoratee
        }
    
        func fetchData(key: String) -> String {
            if let cached = cache[key] {
                print("⚡️ Returning from cache")
                return cached
            }
            let data = decoratee.fetchData(key: key)
            cache[key] = data
            return data
        }
    }

    When executed, both calls to fetchData return identical results. From the caller’s perspective, the behavior is indistinguishable; under the hood, however, different paths were taken: the first call fetched fresh data from the remote source, while the second served it directly from the cache.

    let service: DataService = CacheDecorator(decoratee: NetworkDataService())
    
    print(service.fetchData(key: "user")) // 🌐 Fetching from network... -> Data for user
    print(service.fetchData(key: "user")) // ⚡️ Returning from cache   -> Data for user

    Fallback / Output formatter

    In this scenario, the decorator acts exclusively on the output. While the formatted responses look different, they represent the exact same underlying value—a concept demonstrated by the following Currency Decorator example. Although the decorator returns different strings depending on the selected currency (e.g., USD or EUR), the monetary value remains identical after conversion.

    import Foundation
    
    // 1. Component Protocol
    protocol PriceService {
        func getFormattedPrice(for productID: String) async -> String
    }
    
    // 2. Concrete Component (Base: USD Format -> $100.50)
    struct BaseUSDPriceService: PriceService {
        private let rawPrices: [String: Decimal] = ["PROD-1": 100.50]
    
        func getFormattedPrice(for productID: String) async -> String {
            let price = rawPrices[productID] ?? 0.00
            let formatter = NumberFormatter()
            formatter.numberStyle = .currency
            formatter.currencySymbol = "$"
            formatter.currencyGroupingSeparator = ","
            formatter.currencyDecimalSeparator = "."
            
            return formatter.string(from: price as NSDecimalNumber) ?? "$\(price)"
        }
    }
    
    // 3. Decorator (Output Formatter: Converts USD -> EUR Format -> 100,50 €)
    struct EURCurrencyDecorator: PriceService {
        let wrapped: PriceService
        let usdToEurRate: Decimal = 0.92 // Exchange rate conversion
    
        func getFormattedPrice(for productID: String) async -> String {
            // 1. Get raw formatted output from wrapped USD service
            let usdString = await wrapped.getFormattedPrice(for: productID)
            
            // 2. Extract numeric value from USD string
            let cleanedString = usdString.replacingOccurrences(of: "$", with: "")
                                         .replacingOccurrences(of: ",", with: "")
            guard let usdAmount = Decimal(string: cleanedString) else { return "0,00 €" }
    
            // 3. Convert currency and reformat (Symbol on the RIGHT)
            let eurAmount = usdAmount * usdToEurRate
            
            let formatter = NumberFormatter()
            formatter.numberStyle = .decimal
            formatter.minimumFractionDigits = 2
            formatter.maximumFractionDigits = 2
            formatter.decimalSeparator = ","
            formatter.groupingSeparator = "."
    
            let formattedNumber = formatter.string(from: eurAmount as NSDecimalNumber) ?? "\(eurAmount)"
            
            // Action on Exit: Transformed into EUR format
            return "\(formattedNumber) €" 
        }
    }

    To support a new currency, simply create another decorator that handles the conversion from USD to that specific currency.

    // Base Service (USD)
        let usdService: PriceService = BaseUSDPriceService()
        let usdPrice = await usdService.getFormattedPrice(for: "PROD-1")
        print("USD Base Output: \(usdPrice)") 
        // Output: $100.50
    
        // Decorator (EUR)
        let eurService: PriceService = EURCurrencyDecorator(wrapped: usdService)
        let eurPrice = await eurService.getFormattedPrice(for: "PROD-1")
        print("EUR Decorator Output: \(eurPrice)") 
        // Output: 92,46 €

    Transformer

    In this scenario, the decorator transforms inputs on the way in and reverses the operation on outputs on the way out. This dual role is well illustrated by a cryptography layer: while the client always interacts with clear, unencrypted data, the decorator automatically ciphers it before storage and deciphers it upon retrieval.

    import Foundation
    import CryptoKit
    
    // 1. Interface & Raw Storage (Decoratee)
    protocol StorageService {
        func save(id: String, payload: Data)
        func load(id: String) -> Data?
    }
    
    class RawStorageService: StorageService {
        private var db = [String: Data]()
    
        func save(id: String, payload: Data) {
            db[id] = payload
            print("💾 [Storage] Saved \(payload.count) raw bytes for '\(id)'")
        }
    
        func load(id: String) -> Data? {
            print("📖 [Storage] Reading raw bytes for '\(id)'")
            return db[id]
        }
    }
    
    // 2. Transformer Decorator (Encrypts on save, Decrypts on load)
    struct CryptoDecorator: StorageService {
        let wrapped: StorageService
        let key: SymmetricKey
    
        func save(id: String, payload: Data) {
            // ENTRANCE: Encrypt Plaintext -> Ciphertext before storing
            guard let sealedBox = try? AES.GCM.seal(payload, using: key),
                  let ciphertext = sealedBox.combined else { return }
            
            print("🔐 [Crypto] Encrypted \(payload.count) bytes -> \(ciphertext.count) bytes")
            wrapped.save(id: id, payload: ciphertext)
        }
    
        func load(id: String) -> Data? {
            // EXIT: Fetch Ciphertext -> Decrypt back to Plaintext
            guard let ciphertext = wrapped.load(id: id),
                  let box = try? AES.GCM.SealedBox(combined: ciphertext),
                  let plaintext = try? AES.GCM.open(box, using: key) else { return nil }
            
            print("🔓 [Crypto] Decrypted \(ciphertext.count) bytes back to original plaintext")
            return plaintext
        }
    }

    Its usage is as follows:

    let key = SymmetricKey(size: .bits256)
    let storage: StorageService = CryptoDecorator(wrapped: RawStorageService(), key: key)
    
    let originalData = "Secret User Token".data(using: .utf8)!
    
    // 1. Save (Encrypts on Entry)
    storage.save(id: "user_session", payload: originalData)
    
    // 2. Load (Decrypts on Exit)
    if let loadedData = storage.load(id: "user_session"),
       let decryptedString = String(data: loadedData, encoding: .utf8) {
        print("Received: '\(decryptedString)'")
    }

    Compositon of decocators

    Last but not least, composing multiple decorators is one of the core strengths of the Decorator pattern. Because every decorator implements the exact same interface as the component it wraps, you can seamlessly chain them together—nesting behavior like Russian dolls.


    Here is a complete usage example combining two separate decorators around a single storage component:

    let key = SymmetricKey(size: .bits256)
    let rawStorage = RawStorage()
    
    // Composition Chain: Client -> MonitorDecorator -> CryptoDecorator -> RawStorage
    let pipeline: StorageService = MonitorDecorator(
        wrapped: CryptoDecorator(
            wrapped: rawStorage,
            key: key
        )
    )
    
    let payload = "Secret Data".data(using: .utf8)!
    
    // Execution sequence:
    // 1. Monitor starts timer -> Calls Crypto
    // 2. Crypto encrypts payload -> Calls RawStorage
    // 3. RawStorage saves to memory
    // 4. Returns back through layers -> Monitor logs total duration
    pipeline.save(id: "config", payload: payload)
    let loaded = pipeline.load(id: "config")

    Its usage is as follows:

    let key = SymmetricKey(size: .bits256)
    let storage: StorageService = CryptoDecorator(wrapped: RawStorageService(), key: key)
    
    let originalData = "Secret User Token".data(using: .utf8)!
    
    // 1. Save (Encrypts on Entry)
    storage.save(id: "user_session", payload: originalData)
    
    // 2. Load (Decrypts on Exit)
    if let loadedData = storage.load(id: "user_session"),
       let decryptedString = String(data: loadedData, encoding: .utf8) {
        print("Received: '\(decryptedString)'")
    }

    Conclusions

    Although the Decorator pattern can adopt different functional roles, confusing them for separate patterns is easy. At its core, the essence remains unchanged: adding or removing features dynamically alters capabilities, but never alters the fundamental nature of the underlying behavior.

    References

    There are two primary historical references linked to the formal documentation of the Decorator pattern:

    • 1994 (GoF Book): Design Patterns: Elements of Reusable Object-Oriented Software — Addison-Wesley published the definitive text defining the pattern.

    • 1991 (ET++ Framework / PhD Thesis): Erich Gamma’s doctoral dissertation at the University of Zurich, where early architectural precursors were documented prior to the GoF book.