Swift and iOS dev with Igor
The ride of the iOS developer

The Decorator Pattern with Protocol and Protocol Witnesses

Gang of Four Design Patterns:

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Essential Developer: Design Patterns in iOS/Swift: Standing on the shoulder of giants | iOS Lead Essentials Podcast #014:

The Decorator pattern offers a way of adding behavior to an individual object and extending its functionality without subclassing or changing the object’s class.

Decorators are useful when you want to add or alter the behavior of individual objects instead of an entire class of objects.

To implement the Decorator pattern, you create a new object (decorator) that encloses and conforms to the interface of the component (decoratee) it decorates. The decorator class will contain the extended behavior and forward messages to the decoratee.

By doing so, the decorator can be used by the clients of the interface, extending the behavior of the system without needing to alter any existing components.

The Decorator pattern is supported by the SOLID principles, especially the Single Responsibility, Liskov Substitution, and Open/Closed Principles.

You can use Decorators to add Cross-Cutting concerns such as Logging, Analytics, Threading, Security, etc. into your modules in a clean way while maintaining low coupling in your applications.

Decorator with protocol

protocol Logger {
    func log(_ message: String)
}

final class PrintLogger: Logger {
    func log(_ message: String) {
        print(message)
    }
}

protocol SendMessageInput {
    associatedtype MessageDetails
    associatedtype ChannelID

    func send(_ messageDetails: MessageDetails, to channelID: ChannelID) throws
}

final class DummySendMessageInput: SendMessageInput {
    typealias MessageDetails = String
    typealias ChannelID = Int

    func send(_ messageDetails: MessageDetails, to channelID: ChannelID) throws {
        // do nothing
    }
}

Decorator

Let’s extend the functionally of the DummySendMessageInput class by logging:

To create decorator with protocols we need a new class, conforming to SendMessageInput, and initialized with logger and decoratee

final class DecoratedDummySendMessageInput: SendMessageInput {
    private let logger: Logger
    private let decoratee: DummySendMessageInput

    init(logger: Logger, decoratee: DummySendMessageInput) {
        self.logger = logger
        self.decoratee = decoratee
    }
}

Extended functionality

     func send(_ messageDetails: String, to channelID: String) throws {
        logger.log(“Started sending message.”)
        try self.send(messageDetails, to: channelID)
        logger.log(“Started sending message.”)
    }
}

Decorator with Protocol Witness

Corresponds to protocol Logger

struct Logging {
    let log: (String) -> Void
}

Protocol witness

extension Logging {
    static let printLogging: Self = .init { print($0) }
}

Corresponds to protocol SendMessageInput

struct SendingMessageInput<MessageDetails, ChannelID> {
    let send: (MessageDetails, ChannelID) throws -> Void
}

Protocol witness, corresponds to DummySendMessageInput

extension SendingMessageInput
where MessageDetails == String,
      ChannelID == String {
    static var dummy: Self { .init { _, _ in } }
}

Decorator

To create decorator with protocol witnesses we do not need a new type, just a function that extends existing type with injected instance of Logging

extension SendingMessageInput {
    func decorated(with logger: Logging) -> Self {
        .init { messageDetails, channelID in
            logger.log(“Started sending message.”)
            try send(messageDetails, channelID)
            logger.log(“Started sending message.”)
        }
    }
}

let decoratedStringDummy: SendingMessageInput = .stringDummy.decorated(with: .printLogging)

References

Published on: Aug 25, 2022
Tagged with: