Декоратор на Swift
Декоратор — це структурний патерн, який дозволяє додавати «на льоту» нові поведінки об’єктам, розміщаючи їх в об’єктах-обгортках.
Декоратор дозволяє загортати об’єкти безліч разів завдяки тому, що і обгортки, і реальні об’єкти, що загортаються, мають спільний інтерфейс.
Складність:
Популярність:
Застосування: Патерн можна часто зустріти в Swift-коді, особливо якщо код створено для роботи з потоками даних.
Ознаки застосування патерна: Декоратор можна розпізнати за створенними методами, які приймають в параметрах об’єкти того ж абстрактного типу чи інтерфейсу, що і поточний клас.
Концептуальний приклад
Цей приклад показує структуру патерна Декоратор, а саме — з яких класів він складається, які ролі ці класи виконують і як вони взаємодіють один з одним.
Після ознайомлення зі структурою, вам буде легше сприймати наступний приклад, що розглядає реальний випадок використання патерна в світі Swift.
Example.swift: Приклад структури патерна
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: Результат виконання
Client: I've got a simple component Result: ConcreteComponent Client: Now I've got a decorated component Result: ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent)) Життєвий приклад
Example.swift: Життєвий приклад
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: Результат виконання
Client: set up an editors stack BlurFilter applies changes Resizer applies changes Image applies changes Client: all changes have been applied for Image