Decorator is a structural pattern that allows adding new behaviors to objects dynamically by placing them inside special wrapper objects, called decorators .
Using decorators you can wrap objects countless number of times since both target objects and decorators follow the same interface. The resulting object will get a stacking behavior of all wrappers.
Conceptual Example
This example illustrates the structure of the Decorator design pattern and focuses on the following questions:
What classes does it consist of?
What roles do these classes play?
In what way the elements of the pattern are related?
After learning about the pattern’s structure it’ll be easier for you to grasp the following example, based on a real-world Swift use case.
Example.swift: Conceptual example
import XCTest
/// The base Component interface defines operations that can be altered by
/// decorators.
protocol Component {
func operation() -> String
}
/// Concrete Components provide default implementations of the operations. There
/// might be several variations of these classes.
class ConcreteComponent: Component {
func operation() -> String {
return "ConcreteComponent"
}
}
/// The base Decorator class follows the same interface as the other components.
/// The primary purpose of this class is to define the wrapping interface for
/// all concrete decorators. The default implementation of the wrapping code
/// might include a field for storing a wrapped component and the means to
/// initialize it.
class Decorator: Component {
private var component: Component
init(_ component: Component) {
self.component = component
}
/// The Decorator delegates all work to the wrapped component.
func operation() -> String {
return component.operation()
}
}
/// Concrete Decorators call the wrapped object and alter its result in some
/// way.
class ConcreteDecoratorA: Decorator {
/// Decorators may call parent implementation of the operation, instead of
/// calling the wrapped object directly. This approach simplifies extension
/// of decorator classes.
override func operation() -> String {
return "ConcreteDecoratorA(" + super.operation() + ")"
}
}
/// Decorators can execute their behavior either before or after the call to a
/// wrapped object.
class ConcreteDecoratorB: Decorator {
override func operation() -> String {
return "ConcreteDecoratorB(" + super.operation() + ")"
}
}
/// The client code works with all objects using the Component interface. This
/// way it can stay independent of the concrete classes of components it works
/// with.
class Client {
// ...
static func someClientCode(component: Component) {
print("Result: " + component.operation())
}
// ...
}
/// Let's see how it all works together.
class DecoratorConceptual: XCTestCase {
func testDecoratorConceptual() {
// This way the client code can support both simple components...
print("Client: I've got a simple component")
let simple = ConcreteComponent()
Client.someClientCode(component: simple)
// ...as well as decorated ones.
//
// Note how decorators can wrap not only simple components but the other
// decorators as well.
let decorator1 = ConcreteDecoratorA(simple)
let decorator2 = ConcreteDecoratorB(decorator1)
print("\nClient: Now I've got a decorated component")
Client.someClientCode(component: decorator2)
}
}
Output.txt: Execution result
Client: I've got a simple component
Result: ConcreteComponent
Client: Now I've got a decorated component
Result: ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))
Real World Example
Example.swift: Real world example
import UIKit
import XCTest
protocol ImageEditor: CustomStringConvertible {
func apply() -> UIImage
}
class ImageDecorator: ImageEditor {
private var editor: ImageEditor
required init(_ editor: ImageEditor) {
self.editor = editor
}
func apply() -> UIImage {
print(editor.description + " applies changes")
return editor.apply()
}
var description: String {
return "ImageDecorator"
}
}
extension UIImage: ImageEditor {
func apply() -> UIImage {
return self
}
open override var description: String {
return "Image"
}
}
class BaseFilter: ImageDecorator {
fileprivate var filter: CIFilter?
init(editor: ImageEditor, filterName: String) {
self.filter = CIFilter(name: filterName)
super.init(editor)
}
required init(_ editor: ImageEditor) {
super.init(editor)
}
override func apply() -> UIImage {
let image = super.apply()
let context = CIContext(options: nil)
filter?.setValue(CIImage(image: image), forKey: kCIInputImageKey)
guard let output = filter?.outputImage else { return image }
guard let coreImage = context.createCGImage(output, from: output.extent) else {
return image
}
return UIImage(cgImage: coreImage)
}
override var description: String {
return "BaseFilter"
}
}
class BlurFilter: BaseFilter {
required init(_ editor: ImageEditor) {
super.init(editor: editor, filterName: "CIGaussianBlur")
}
func update(radius: Double) {
filter?.setValue(radius, forKey: "inputRadius")
}
override var description: String {
return "BlurFilter"
}
}
class ColorFilter: BaseFilter {
required init(_ editor: ImageEditor) {
super.init(editor: editor, filterName: "CIColorControls")
}
func update(saturation: Double) {
filter?.setValue(saturation, forKey: "inputSaturation")
}
func update(brightness: Double) {
filter?.setValue(brightness, forKey: "inputBrightness")
}
func update(contrast: Double) {
filter?.setValue(contrast, forKey: "inputContrast")
}
override var description: String {
return "ColorFilter"
}
}
class Resizer: ImageDecorator {
private var xScale: CGFloat = 0
private var yScale: CGFloat = 0
private var hasAlpha = false
convenience init(_ editor: ImageEditor, xScale: CGFloat = 0, yScale: CGFloat = 0, hasAlpha: Bool = false) {
self.init(editor)
self.xScale = xScale
self.yScale = yScale
self.hasAlpha = hasAlpha
}
required init(_ editor: ImageEditor) {
super.init(editor)
}
override func apply() -> UIImage {
let image = super.apply()
let size = image.size.applying(CGAffineTransform(scaleX: xScale, y: yScale))
UIGraphicsBeginImageContextWithOptions(size, !hasAlpha, UIScreen.main.scale)
image.draw(in: CGRect(origin: .zero, size: size))
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return scaledImage ?? image
}
override var description: String {
return "Resizer"
}
}
class DecoratorRealWorld: XCTestCase {
func testDecoratorRealWorld() {
let image = loadImage()
print("Client: set up an editors stack")
let resizer = Resizer(image, xScale: 0.2, yScale: 0.2)
let blurFilter = BlurFilter(resizer)
blurFilter.update(radius: 2)
let colorFilter = ColorFilter(blurFilter)
colorFilter.update(contrast: 0.53)
colorFilter.update(brightness: 0.12)
colorFilter.update(saturation: 4)
clientCode(editor: colorFilter)
}
func clientCode(editor: ImageEditor) {
let image = editor.apply()
/// Note. You can stop an execution in Xcode to see an image preview.
print("Client: all changes have been applied for \(image)")
}
}
private extension DecoratorRealWorld {
func loadImage() -> UIImage {
let urlString = "https:// refactoring.guru/images/content-public/logos/logo-new-3x.png"
/// Note:
/// Do not download images the following way in a production code.
guard let url = URL(string: urlString) else {
fatalError("Please enter a valid URL")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Cannot load an image")
}
guard let image = UIImage(data: data) else {
fatalError("Cannot create an image from data")
}
return image
}
}
Output.txt: Execution result
Client: set up an editors stack
BlurFilter applies changes
Resizer applies changes
Image applies changes
Client: all changes have been applied for Image