1. Введение
Тема интерфейсов часто сбивает с толку начинающего разработчика. При первом знакомстве они кажутся странным излишком, ненужным при наличии наследования у классов. Зачем с помощью интерфейсов описывать класс в одном месте, чтобы написать его в итоге в другом? В дальнейшем буду описывать всё для языка Swift, где интерфейсы принято называть "протоколы", но логика в остальном не сильно отличается, синтаксис будет понятен при начальных знаниях любого другого языка.
Протоколы не просто про "костяк класса". Идейная их суть в том, чтобы выдвигать конкретные требования к классам и давать за это взамен возможность модульно работать с этим классом на чуть более высоком уровне абстракции.
Класс, "соглашаясь" реализовывать протокол, даёт возможность своим экземплярам реализовывать спрашиваемый с них кем-то другим функционал. Это позволяет кому-то извне, кто работает с этим экземпляром, "доверять" ему, потому что эти обязательства проверяются IDE ещё на процессе сборки, а чаще и вовсе на этапе написания кода. Как известно, ошибки остающиеся в редакторе - хорошие ошибки.
2. Теоретический пример для самых маленьких
Представим, что мы хотим устроить птичий концерт. Хотим иметь список выступающих и просить их спеть в нужном порядке. Как бы мы решили эту задачу с обычными классами без непонятных протоколов?
-
Заводим класс Птица
-
Определяем свойство "Имя" типа String
-
Определяем функцию "Спой" сигнатуры ()->(), т.е. ничего не принимает, ничего не возвращает, внутри только print(текстПесни)
-
Определяем инициализатор
-
Готово! Можно проводить концерт! Делаем массив Птиц и каждую птичку зовём на сцену циклом for-in
Но что, если для некоторых мы хотим в ином контексте иной функциональности? Может некоторые хотят спеть дважды или трижды?
-
Заводим список всех возможных птиц и пишем каждой свои функции, сравнивая постоянно имя по огромному статическому массиву или енуму
Но что если мы хотим ещё и большей переиспользуемости кода без if'ов и множества switch-case конструкций?
-
Каждая птичка будет отдельной сущностью, будет наследовать класс Птица и переопределять функции оттуда.
Неплохой вариант для работы самому, когда видишь код целиком и пишешь за один присест, но если решишь позвать своего друга помочь забивать вручную всех птиц, то кто, кроме тебя, ему подскажет какие методы переопределять?
-
Можно приписать нужным методам required, требуя их переопределения для каждого потомка.
Но что делать, если мы не хотим апкастить и даункастить каждый раз подгоняя тип под нужный контекст, опасаясь конфликта в рантайме и отложенных ошибок? Кроме этого было бы неплохо уже на этом моменте представлять всё как-то сильно проще. Может есть ещё варианты?
Если на этом моменте вы не ушли писать свою систему автокастов под данный случай и запутанные модели данных на классах с изобретениями множества велосипедов, то хочу вам представить протоколы.
3. Практический пример в Xcode
Вместо того, чтобы пытаться работать с птичками через суперклассы, когда от одной Птицы происходят все остальные певчие пернатые, лучше как-то отмечать уже существующих птиц умением петь. Да так, чтобы всех умеющих петь можно было вместе в один концерт собрать без лишних проблем. Тут и поможет "певчий" протокол.
protocol СпособноПеть { //обязательство уметь петь
func спой() //требование реализовать умение петь
}
Для самых-самых маленьких и любопытных
Если вы впервые столкнулись с Swift, а если и с разработкой вообще, то для данной статьи весь используемый код я писал в одном Playground файле Xcode, при его изучении советую делать также. Что такое Playground в Xcode и как с ним изучать Swift можно спросить в Google.
При желании покопать код на практике, поменять классы или побаловаться со всякой магией, можете скопировать все интересующие блоки ниже и потренироваться тут.
В случае отсутствия компьютера на macOS или желания качать Xcode, скомпилировать код из данной статьи и посмотреть на его работу можно также по ссылке выше.
Всё, теперь каждая птичка, что захочет выступить на концерте, должна будет уметь петь и реализовать этот протокол.
class Петух : СпособноПеть {
func спой() {
print("Ку-ка-ре-куууууууу")
}
}
class Кукушка : СпособноПеть {
func спой() {
print("Ку-ку ку-ку ку-ку")
}
}
Что будет, если кто-то из птичек захочет нас обмануть?
Компилятор зорко подметит такое безобразие и заботливо сообщит об этом, предложив при этом решить данный казус. А вы думали Xcode только ругаться умеет?
Отлично, мы научили птичек петь и умеем понимать кто правда умеет, а кто только притворяется. Но что делать с выступлениями? Концерт должен состояться!
class Выступление { //схема выступления конкретного певца
let певец : СпособноПеть //да-да, прям так, как полноценный тип
init(предлагаемыйПевец : СпособноПеть) {
self.певец = предлагаемыйПевец
}
}
За счёт того, что певцом в выступлении мы определили объект, который должен реализовывать протокол СпособноПеть, то и взаимодействовать с ним мы теперь будем через этот протокол. Все певцы для нас теперь равны и отвечают правилам, на которые согласились, подписывая особый "договор"
Согласно этому договору они, пока что, умеют только петь. Даже если мы добавим одному из выступающих особый навык в его класс, то не сможем попросить исполнить его, пока не внесём одноименную функцию той же сигнатуры в протокол.
Но если мы и правда устроим скандал и отредактируем уже существующий договор, добавив в протокол функцию каркать, то реализовать его будут вынуждены и все ранее подписавшиеся, о чём мы их вообще не просили.
Однако подобные ограничения для певца ощутимо развязывают руки для нас. Вы заметили как мы перешли от конкретных птичек до более общего понятия "певец"? И не спроста, потому что "певцом" теперь может быть кто угодно. Да-да, кто угодно, кто подписывает договор и продаёт душу дьяволу реализует протокол СпособноПеть, причём внутри можем творить вообще любую дичь. Главное - факт реализации, а детали уже за нами.
extension String : СпособноПеть { //дату можем вывести, почему бы нет
func спой() {
print(self)
let currentDate = NSDate.now
print("Вижу я, что сегодняшняя дата - (currentDate)")
} //любой каприз за ваши вычислительные мощности, как говорится
}
Теперь почти любой класс, к которому можно прикрутить соответствующие протоколу расширения, способен научиться петь за время выполнения данного кода и участвовать в нашем концерте. Весьма оперативно, скажу я. Да, вам музыкальную школу открывать пора.
4. Show time!
Теперь всё готово, осталось только позвать выступающих и начать шоу.
//Зовём всех!
let воронаКаркуша = Ворона() //позвали Каркушу
let петухПетя = Петух() //позвали Петю
let кукушкаВасилиса = Кукушка() //позвали Василису
//Ставим с каждым уникальный перфоманс
let номерКаркуши = Выступление(предлагаемыйПевец: воронаКаркуша)
let номерПети = Выступление(предлагаемыйПевец: петухПетя)
let номерВасилисы = Выступление(предлагаемыйПевец: кукушкаВасилиса)
//Определем очерёдность всех выступлений и собираем сцену
let концерт = [номерКаркуши, номерПети, номерВасилисы]
концерт.forEach {выступление in выступление.певец.спой()}
//если непонятно написанное строчкой выше почитайте про Closures в Swift
Результат:
Вы провели шикарный концерт, сделали первые шаги в карьере музыкального продюсера, примерно догадываетесь что же из себя представляют протоколы, но что ещё важнее, сегодня благодаря новому материалу удалось добиться неплохой модульности.
Ни один из классов, что реализует протокол выше не знает друг о друге, и способен делать что душе вздумается независимо от других, пока соответствует протоколу. Можете в отдельные файлы вынести, если хотите. Над ними даже разные люди работать могут, даже новые модули добавлять без опаски что-то сломать, компилятор не даст наломать дров, т.е. оно легко читается, понимается и расширяется. Profit!
Заключение
Что это всё значит? Все классы выше - самодостаточные модули, что связаны лишь общим протоколом и хорошо покрываются автотестами и могут быть переиспользованы в будущем независимо от других. Более продвинутая функциональность языка позволи не просто одиночные концерты, а настоящие мировые турне устраивать, при особом полёте фантазии не ограничиваясь ни планетой, ни измерением, ни временем, но об этом уже как-нибудь в другой раз.
Подводя итог можно сказать, что протоколы - крайне мощный инструмент. В рамках языка Swift настолько, что на одной из WWDC(Apple Worldwide Developers Conference) Swift не без оснований назвали протокол-ориентированным языком. Надеюсь, я смог чуть прояснить протоколы для вас или, как минимум, достаточно заинтересовать для самостоятельного изучения.
Замечание про намеренно неоптимальный код
Развитие показательно ложной идеи в п.2 призвано лишь привести к необходимости протоколов в данном примере, я не исключаю оптимального варианта реализации функционала данного простого примера на классах, однако хотел больше показать до чего может привести малое количество опыта с неистовым желанием кулхацкерить и изобретать свои велосипеды, вместо изучения функций языка и кажущегося сложным(и ненужным) материала
Спасибо за внимание! Я только начинаю свой путь в iOS-разработке, но хочу периодически писать подобные информационно-развлекательные материалы на разные темы по мере продвижения по изучаемому материалу и буду рад замечаниям от более опытных разработчиков.
Автор: Андрей Жаров