Menu
Coddy logo textTech

Паттерн декоратор

Часть раздела Объектно-ориентированное программирование путешествия по Swift на Coddy. Урок 47 из 57.

Паттерн декоратор добавляет поведение объекту, оборачивая его в другой объект с тем же протоколом. Обёртка перенаправляет вызов обёрнутому объекту и добавляет свою часть до или после него:

protocol Coffee {
    func cost() -> Int
    func label() -> String
}

struct Espresso: Coffee {
    func cost() -> Int { 200 }
    func label() -> String { "espresso" }
}

struct WithMilk: Coffee {
    let base: any Coffee
    func cost() -> Int { base.cost() + 50 }
    func label() -> String { base.label() + " + milk" }
}

let drink = WithMilk(base: Espresso())
print(drink.label())
print(drink.cost())

Вывод:

espresso + milk
250

Поскольку декоратор сам является Coffee, декораторы могут оборачивать друг друга в любой комбинации и порядке, без отдельного подкласса для каждой комбинации:

protocol Coffee { func cost() -> Int; func label() -> String }
struct Espresso: Coffee { func cost() -> Int { 200 }; func label() -> String { "espresso" } }
struct WithMilk: Coffee {
    let base: any Coffee
    func cost() -> Int { base.cost() + 50 }
    func label() -> String { base.label() + " + milk" }
}
struct WithSyrup: Coffee {
    let base: any Coffee
    func cost() -> Int { base.cost() + 30 }
    func label() -> String { base.label() + " + syrup" }
}

let fancy = WithSyrup(base: WithMilk(base: WithMilk(base: Espresso())))
print(fancy.label())
print(fancy.cost())

Результат:

espresso + milk + milk + syrup
330

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

protocol DataSource { func fetch(_ id: Int) -> String }
struct Database: DataSource { func fetch(_ id: Int) -> String { "row \(id)" } }

struct Logged: DataSource {
    let inner: any DataSource
    func fetch(_ id: Int) -> String {
        print("fetching \(id)")
        return inner.fetch(id)
    }
}

let source: any DataSource = Logged(inner: Database())
print(source.fetch(7))

Вывод:

fetching 7
row 7

Код, использующий объект, видит только протокол, поэтому не может отличить декорированный объект от обычного. Именно это делает шаблон безопасным для последующего добавления.

func printReceipt(_ c: any Coffee) {      // работает для Espresso и для любой обёрнутой версии
    print("\(c.label()): \(c.cost())")
}
challenge icon

Задание

Легко

Пиццы создаются с помощью декораторов. Протокол Pizza содержит price() и toppings() (массив названий). Base имеет цену 800 и не содержит начинок. Реализуйте декораторы Cheese (+150), Olives (+100) и Double (удваивает цену всего, что находится внутри него, и повторяет его начинки дважды). Завершите реализацию build(_:): она начинает с Base и оборачивает его одним декоратором для каждого названия по порядку.

Предоставленный код считывает строки с разделёнными запятыми названиями начинок (или пустую строку), создаёт каждую пиццу и выводит 1050: cheese, olives или 800: plain.

Ваш код должен находиться в файлах Pizza.swift, Base.swift, Cheese.swift, Olives.swift, Double.swift и Functions.swift. В файле main.swift находится предоставленный код ввода/вывода, и его нельзя изменять.

Попробуйте сами

// Предоставленный код ввода/вывода: оставьте его как есть
var input: [String] = []
while let line = readLine() { input.append(line) }
for line in input {
    let names = line.isEmpty ? [] : line.split(separator: ",").map(String.init)
    let p = build(names)
    let tops = p.toppings()
    print("\(p.price()): " + (tops.isEmpty ? "plain" : tops.joined(separator: ", ")))
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Песочница Swift