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.
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
- Gang of Four (GoF) Design Patterns: Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Ralph Johnson, John Vlissides, Richard Helm