스위프트로 작성된 데코레이터
데코레이터는 구조 패턴이며 새로운 행동들을 특수 래퍼 객체들 내에 넣어서 이러한 행동들을 객체들에 동적으로 추가할 수 있도록 합니다.
데코레이터를 사용하여 객체들을 제한 없이 래핑할 수 있습니다. 왜냐하면 대상 객체들과 데코레이터들은 같은 인터페이스를 따르기 때문입니다. 결과 객체는 모든 래퍼의 스태킹된 행동을 가질 것입니다.
복잡도:
인기도:
사용 예시들: 데코레이터는 스위프트 코드, 특히 스트림과 관련된 코드에서 꽤 표준적입니다.
식별: 데코레이터는 같은 클래스의 객체 또는 인터페이스를 현재 클래스로 수락하는 생성 메서드들 또는 생성자들로 인식할 수 있습니다.
개념적인 예시
이 예시는 데코레이터 패턴의 구조를 보여주고 다음 질문에 중점을 둡니다:
- 패턴은 어떤 클래스들로 구성되어 있나요?
- 이 클래스들은 어떤 역할을 하나요?
- 패턴의 요소들은 어떻게 서로 연관되어 있나요?
이 패턴의 구조를 배우면 실제 스위프트 사용 사례를 기반으로 하는 다음 예시를 더욱 쉽게 이해할 수 있을 것입니다.
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