Swift Design Patterns: Decorator

Зачем нужен?

Паттерн Decorator применяется для динамического добавления поведения или состояния объектам. Наследования не является допустимой альтернативой, т.к оно статично(этап компиляции) и применяется ко всему классу.

Соответствует open/closed принципу SOLID: класс открыт для расширения, но закрыт для изменения

Определение

  • динамически наделяет объект новыми возможностями и является гибкой альтернативой наследованию в области расширения функциональности
  • декорирование базового объекта путем рекурсивного завертывания(wrapping) дополнений, определяемых пользователем

Реализация

  1. Определите общий интерфейс(таким образом классы будут взаимозаменяемы)
  2. Создайте класс основного компонента(наследует общий интерфейс)
  3. Создайте базовый класс декоратора(наследует общий интерфейс)
  4. У декоратора с помощью композиции объявите переменную типа общего интерфейса
  5. Инициализируйте этот объект в инициализаторе декоратора
  6. У декоратора делегируйте методы общего интерфейса этому объекту
  7. Создайте конкретные классы декоратора для каждого дополнения
  8. Конкретные классы декоратора делегируют поведение базовому классу декоратора + добавляют свое
  9. Клиент конфигурирует тип и порядок базового и дополнительных компонентов

Пример

Допустим мы владеем кафе и у нас в продаже имеются различные виды кофе:

  • кофе темной обжарки
  • кофе с шоколадом
  • кофе со взбитыми сливками

Можно реализовать базовый класс кофе и наследовать различные варианты и комбинации конкретный типов кофе, т.о получим такой набор классов:

  • базовый класс кофе
  • кофе темной обжарки
  • кофе темной обжарки с шоколадом
  • кофе темной обжарки со взбитыми сливками
  • кофе темной обжарки с шоколадом и взбитыми сливками
  • ……….

Другой способ решения - применение декоратора: инкапсулируем исходный объект в интерфейсе, наследуем полученный интерфейс и основным объектом, и декоратором. При этом добавляем с помощью композиции переменную с типом интерфейса декоратору и переопределяем методы на использование делегирования. Создаем конкретные дополнения, делегируем базовому декоратору и добавляем конкретное поведение.

Определяем общий интерфейс

// абстрактный компонент, общий интерфейс
protocol Beverage {
    func cost() -> Double
    func description() -> String
}

Создаем классы основных компонентов

// конкретный компонент
class Espresso: Beverage {
    func description() -> String {
        return "Espresso"
    }
    
    func cost() -> Double {
        return 1.99
    }
}

// конкретный компонент
class DarkRoast: Beverage {
    func description() -> String {
        return "Dark Roast"
    }
    
    func cost() -> Double {
        return 2.99
    }
}

Создаем базовый класс декоратора, с помощью композиции объявляем переменную типа общего интерфейса, инициализируем переменную в инициализаторе и делегируем методы общего интерфейса(пункты 3, 4, 5, 6)

// абстрактный декоратор
class CondimentDecorator: Beverage {
    private var decoratedBeverage: Beverage
    
    init(beverage: Beverage) {
        decoratedBeverage = beverage
    }
    
    func description() -> String {
        return decoratedBeverage.description()
    }
    
    func cost() -> Double {
        return decoratedBeverage.cost()
    }
}

Создаем конкретные классы декоратора, делегируем поведение базовому декоратору и добавляем свое(пункты 7, 8)

// конкретное дополнение
class Mocha: CondimentDecorator {
    override func description() -> String {
        return super.description() + ", Mocha"
    }
    
    override func cost() -> Double {
        return super.cost() + 0.5
    }
}

// конкретное дополнение
class Whip: CondimentDecorator {
    override func description() -> String {
        return super.description() + ", Whip"
    }
    
    override func cost() -> Double {
        return super.cost() + 0.2
    }
}

Конфигурируем нужный объект, комбинируя основной компонент и дополнения

var myEspresso:Beverage = Espresso()
// Espresso 1.99

var myDarkRoast:Beverage = DarkRoast()
// Dark Roast 2.99

myDarkRoast = Mocha(beverage: myDarkRoast)
myDarkRoast = Mocha(beverage: myDarkRoast)
myDarkRoast = Whip(beverage: myDarkRoast)
// Dark Roast, Mocha, Mocha, Whip 4.19

В качестве еще одного примера:

  • базовый объект - новогодняя елка
  • класс декоратора - украшения
  • конкретные классы декоратора - игрушки, гирлянда и тд

Реализация паттерна Decorator: