Dekorator to strukturalny wzorzec pozwalający na dodawanie obiektom nowych obowiązków w sposób dynamiczny — poprzez “opakowywanie” ich w specjalne obiekty posiadające potrzebną funkcjonalność.
Stosując dekoratory można opakowywać obiekty wielokrotnie, gdyż zarówno obiekt docelowy jak i dekoratory są zgodne pod względem interfejsu. Wynikowy obiekt będzie posiadał ułożoną w formie stosu połączoną funkcjonalność wszystkich “opakowań”.
Przykład koncepcyjny
Poniższy przykład ilustruje strukturę wzorca Dekorator ze szczególnym naciskiem na następujące kwestie:
Z jakich składa się klas?
Jakie role pełnią te klasy?
W jaki sposób elementy wzorca są ze sobą powiązane?
Poznawszy strukturę wzorca będzie ci łatwiej zrozumieć następujący przykład, oparty na prawdziwym przypadku użycia Swift.
Example.swift: Przykład koncepcyjny
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: Wynik działania
Client: I've got a simple component
Result: ConcreteComponent
Client: Now I've got a decorated component
Result: ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))
Przykład z prawdziwego życia
Example.swift: Przykład z prawdziwego życia
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: Wynik działania
Client: set up an editors stack
BlurFilter applies changes
Resizer applies changes
Image applies changes
Client: all changes have been applied for Image