Menu
Coddy logo textTech

Паттерн делегирования

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

Делегирование позволяет объекту передавать некоторые решения другому объекту — своему делегату — через протокол. Объект выполняет свою работу и обращается к делегату в тех точках, которые могут различаться. Фреймворки Apple используют этот шаблон повсюду:

protocol DownloadDelegate: AnyObject {
    func progress(_ percent: Int)
    func finished(_ file: String)
}

final class Downloader {
    weak var delegate: DownloadDelegate?

    func download(_ file: String) {
        for p in [50, 100] { delegate?.progress(p) }
        delegate?.finished(file)
    }
}

final class Screen: DownloadDelegate {
    func progress(_ percent: Int) { print("\(percent)%") }
    func finished(_ file: String) { print("done: \(file)") }
}

let screen = Screen()
let d = Downloader()
d.delegate = screen
d.download("photo.png")

Вывод:

50%
100%
done: photo.png

Свойство делегата является необязательным, а вызовы используют delegate?., поэтому объект работает даже тогда, когда никто не слушает. Оно объявлено как weak, поэтому объект не удерживает своего делегата самостоятельно, что предотвращает циклы удержания между объектами, которые ссылаются друг на друга:

weak var delegate: DownloadDelegate?   // требует, чтобы протокол был только для классов: AnyObject
delegate?.finished(file)               // ничего не делает, когда нет делегата

Делегат также может отвечать на вопросы, что позволяет настраивать поведение объекта без создания подкласса:

protocol ListDelegate: AnyObject {
    func shouldShow(_ item: String) -> Bool
}

final class ListView {
    weak var delegate: ListDelegate?
    func render(_ items: [String]) -> [String] {
        items.filter { delegate?.shouldShow($0) ?? true }
    }
}

final class HideDrafts: ListDelegate {
    func shouldShow(_ item: String) -> Bool { !item.hasPrefix("draft") }
}

let view = ListView()
let rules = HideDrafts()
print(view.render(["a", "draft b"]))
view.delegate = rules
print(view.render(["a", "draft b"]))

Результат:

["a", "draft b"]
["a"]

Делегирование отличается от шаблона наблюдателя: у объекта есть один делегат, которого он спрашивает и информирует, тогда как у субъекта может быть много наблюдателей, которых он только информирует.

// делегирование: один партнёр, может отвечать на вопросы
weak var delegate: TableDelegate?
// наблюдатель: любое количество слушателей, только уведомляются
private var observers: [(Event) -> Void] = []
challenge icon

Задание

Легко

Checkout запрашивает решения у своего делегата. Протокол CheckoutDelegate (только для классов) содержит discount(for total: Int) -> Int и didComplete(total: Int). В Checkout есть weak var delegate, а pay(_ prices: [Int]) -> Int суммирует цены, вычитает скидку делегата (0 при отсутствии делегата), сообщает делегату итоговую сумму и возвращает её. Реализуйте LoyaltyShop — делегат, который предоставляет скидку 10% при сумме 100 или больше и записывает каждую завершённую сумму в receipts.

Предоставленный код считывает строки с разделёнными запятыми ценами. Для первой строки выполняется оплата без делегата, а для остальных — с LoyaltyShop; выводится каждая итоговая сумма и в конце receipts: 120 90.

Ваш код должен находиться в CheckoutDelegate.swift, Checkout.swift и LoyaltyShop.swift. В main.swift содержится предоставленный код ввода/вывода, и его нельзя редактировать.

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

// Предоставленный код ввода/вывода: оставьте его как есть
var input: [String] = []
while let line = readLine() { input.append(line) }
let checkout = Checkout()
let shop = LoyaltyShop()
for (i, line) in input.enumerated() {
    if i == 1 { checkout.delegate = shop }
    print(checkout.pay(line.split(separator: ",").map { Int($0)! }))
}
print("receipts: " + shop.receipts.map { String($0) }.joined(separator: " "))
quiz iconПроверьте себя

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

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

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