Декоратор — это структурный паттерн, который позволяет добавлять объектам новые поведения на лету, помещая их в объекты-обёртки.
Декоратор позволяет оборачивать объекты бесчисленное количество раз благодаря тому, что и обёртки, и реальные оборачиваемые объекты имеют общий интерфейс.
Концептуальный пример
Этот пример показывает структуру паттерна Декоратор , а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
После ознакомления со структурой, вам будет легче воспринимать второй пример, который рассматривает реальный случай использования паттерна в мире Swift.
Example.swift: Пример структуры паттерна
import XCTest
/// Базовый интерфейс Компонента определяет поведение, которое изменяется
/// декораторами.
protocol Component {
func operation() -> String
}
/// Конкретные Компоненты предоставляют реализации поведения по умолчанию. Может
/// быть несколько вариаций этих классов.
class ConcreteComponent: Component {
func operation() -> String {
return "ConcreteComponent"
}
}
/// Базовый класс Декоратора следует тому же интерфейсу, что и другие
/// компоненты. Основная цель этого класса - определить интерфейс обёртки для
/// всех конкретных декораторов. Реализация кода обёртки по умолчанию может
/// включать в себя поле для хранения завёрнутого компонента и средства его
/// инициализации.
class Decorator: Component {
private var component: Component
init(_ component: Component) {
self.component = component
}
/// Декоратор делегирует всю работу обёрнутому компоненту.
func operation() -> String {
return component.operation()
}
}
/// Конкретные Декораторы вызывают обёрнутый объект и изменяют его результат
/// некоторым образом.
class ConcreteDecoratorA: Decorator {
/// Декораторы могут вызывать родительскую реализацию операции, вместо
/// того, чтобы вызвать обёрнутый объект напрямую. Такой подход упрощает
/// расширение классов декораторов.
override func operation() -> String {
return "ConcreteDecoratorA(" + super.operation() + ")"
}
}
/// Декораторы могут выполнять своё поведение до или после вызова обёрнутого
/// объекта.
class ConcreteDecoratorB: Decorator {
override func operation() -> String {
return "ConcreteDecoratorB(" + super.operation() + ")"
}
}
/// Клиентский код работает со всеми объектами, используя интерфейс Компонента.
/// Таким образом, он остаётся независимым от конкретных классов компонентов, с
/// которыми работает.
class Client {
// ...
static func someClientCode(component: Component) {
print("Result: " + component.operation())
}
// ...
}
/// Давайте посмотрим как всё это будет работать.
class DecoratorConceptual: XCTestCase {
func testDecoratorConceptual() {
// Таким образом, клиентский код может поддерживать как простые
// компоненты...
print("Client: I've got a simple component")
let simple = ConcreteComponent()
Client.someClientCode(component: simple)
// ...так и декорированные.
//
// Обратите внимание, что декораторы могут обёртывать не только простые
// компоненты, но и другие декораторы.
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