Как работает делегирование - Справочник разработчика на Swift

Делегирование может показаться сложной темой, от которой у вас пойдет голова кругом. Я подумал, что стоит разбить ее на отдельные тезисы, чтобы понимание основных моментов стало проще тем, кто еще с ней не знаком.

Итак, с терминологией вы уже знакомы и имеете представление о делегировании, как о шаблоне проектирования. Теперь, самое время ответить на следующий вопрос, а именно: «Как работает делегирование?». Готовы? Приступим!

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

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

Итак, как же один класс делегирует логику поведения другому классу? В iOS в Swift, шаблон делегирования достигается путем использования абстрактной прослойки под названием протокол.

Заметка

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

Протоколы, как абстракции

Я использовал модный термин «абстрактная прослойка» до цитаты, но что это вообще такое?

Протоколы - это "абстракция", потому что они не несут в себе детали реализации... Только имена функций и свойств. Это лишь чертеж, как ее и обозначает Apple.

Протоколы как чертежи

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

То же самое и с протоколом: множество классов могут быть построены в соответствии с его требованиями. В конце концов, реализация каждого класса (все что находится между фигурными скобками {…}) может отличаться, но если они созданы по одному протоколу, то они будут похожи хотя бы тем, что у них есть некоторая функциональность, с одним и тем же именем.

Протоколы как контракты

Существует еще одна популярная аналогия для описания протоколов и взята она из правового мира: протоколы похожи на контракты. Именно эта договорная идея нравится мне больше всего, когда дело доходит до делегирования.

Контракт - это то, что оказывается посередине двух сторон, которые пытаются договориться о сделке. Для одной стороны, договор - это гарантии выполнения некоторых условий, которые должны быть удовлетворены. Для другой стороны, договор - это набор обязательств.

В шаблоне делегирования протоколы выполняют роль того, что находится «по середине», также, как и контракт. Для класса делегатора протокол - это гарантия того, что определенное поведение будет принято другой стороной (делегатом). Для класса делегата протокол - это набор обязательств, которые должны быть реализованы, когда «контракт подписан», или, говоря на языке Swift - «протокол принят».

Так как человек, подписывающий контракт, тоже получает что-то от сделки, то мы, все-таки делаем ударение в сравнении с протоколами и шаблонами делегирования на человека, который получает гарантии.

Человек, чьи гарантии застрахованы контрактом и будут выполнены тем лицом, кто подписывает сделку, волен принимать решения или участие, исходя из этого обещания.

Тоже самое происходит и с классом, делегирующим другому классу. Класс делегатора может выполнять действия (вызывать методы определенные протоколом), или принимать решения (доступ к свойствам определенным протоколом, для использования у себя логики протокола).

Список игроков

Отступив от этого описания, мы видим участие трех игроков:

  1. Протокол, определяющий обязанности, которые будут делегированы
  2. Делегатор, который в некоторой степени зависит от экземпляра того класса, который подписан под протокол
  3. Делегат, который принимает протокол и реализует его потребности

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

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

Пример в коде

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

Настройка делегатора

Класс делегатора обычно определяет переменное свойство с включенным словом «delegate» где-то в имени (часто свойство так и называют  delegate, если этого будет достаточно для его представления). Тип свойства переменной - это ключ ко всему. Свойство будет типа того-же-что-и-протокол-делегирования. Т.е. если я назову протокол MyDelegate, то я определю тип свойства делегата как MyDelegate.

Настройка делегата

Класс делегата принимает и реализует его потребности. В объявлении класса, имя протокола (протоколов), которые класс намерен принять, перечисляются через запятую после названия суперкласса (если класс наследует от суперкласса):

class MyClass: SuperClass, Protocol1, Protocol2 { ... }

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

Делегирование в действии

Большую часть времени мы работаем с Apple APIs (таким как UITableView или любым другим UI элементом, которым они обеспечивают). От нас только требуется знание объявления протокола, так, чтобы класс, который мы выбрали нашим делегатом, мог реализовать правильные функции.

Общий пример

Могут возникнуть ситуации, когда вы решите последовать руководству Apple и использовать шаблон делегирования в своем коде. Может быть, вы создаете свой подкласс UIView или свой UIImagePickerController . Или вы может быть занимаетесь разработкой игр и хотите создать обратную связь SKScene к View Controller’у. Это всего несколько примеров, которые приходят на ум, но все они используют делегирование.

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

Создаем протокол

protocol RatingPickerDelegate {
    func preferredRatingSymbol(picker: RatingPicker) -> UIImage?
    func didSelectRating(picker: RatingPicker, rating: Int)
    func didCancel(picker: RatingPicker)
}

Обратите внимание, что реализация этого протокола позволяет создать и настройку отображения нашего рейтинга, и петлю обратной связи. Это всегда удобно, когда у делегата есть доступ к public API экземпляра, вызывающего его методы, таким образом RatingPicker ( UITableView или UIScrollView) часто передается как аргумент.

Создание делегатора

С уже определенным протоколом, наш RatingPicker (в нашем случае делегатор) может подписать себя под использование этого протокола:

// Спойлер: В этом примере на самом деле намного больше логики, чем вы встретите в реальных приложениях
// Этот код используется не как реальный пример, а как пример, на который может быть похож реальный код,
// который использует делегирование внутри собственной реализации
 
class RatingPicker {
    var delegate: RatingPickerDelegate?
    
    func setup() {
        let preferredRatingSymbol = delegate?.preferredRatingSymbol(self)
        
        // Установите picker с предпочитаемым символом рейтинга, если он был указан
    }
    
    func selectRating(selectedRating: Int) {
        delegate?.didSelectRating(self, rating: selectedRating)
        // Прочая логика, связанная с выбором рейтинга
    }
    
    func cancel() {
        delegate?.didCancel(self)
        // Прочая логика, связанная с отменой
    }
}

Свойство delegate имеет тип RatingPickerDelegate. Так как это свойство delegate опционально, то оно не обязательно для корректной работы класса RatingPicker. Если бы оно было обязательным, мы бы не сделали его опциональным и нам бы пришлось задать ему начальное значение во время инициализации.

Затем мы использовали опциональную последовательность, чтобы добраться до методов delegate, если она не nil.

Выбираем делегата

Выбор класса делегата - последнее решение, которое вам нужно принять. Не так уж редко бывает, что View Controller берет на себя ответственность стать делегатом.

Мы избежали соблазна дать View Controller’у больше ответственности, чем требуется. Теперь давайте создадим простой класс, который реализует протокол.

class RatingPickerHandler: RatingPickerDelegate {
    func preferredRatingSymbol(picker: RatingPicker) -> UIImage? {
        return UIImage(contentsOfFile: "Star.png")
    }
    
    func didSelectRating(picker: RatingPicker, rating: Int) {
        // что-то делаем в зависимости от выбранного изображения
    }
    
    func didCancel(picker: RatingPicker) {
        // делаем что-то в ответ на действие отмены
    }
}

Итог

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