Зачем нужны протоколы и когда их использовать
Вступление
У меня было много собеседований со стартапами и крупными компаниями, и все они задавали мне такие вопросы:
- В чем разница между протоколами и классами?
- Почему бы не использовать суперклассы вместо протоколов?
Эта статья даст вам ответ на эти вопросы и может познакомить вас с новым аспектом программирования на Swift.
Прочитав это, вы поймете, для чего создаются протоколы, когда мы можем их использовать, почему мы должны выбирать их над суперклассом и каковы их плюсы и минусы.
Давай перестанем говорить и начнем делать!
Проблемы наследования
При переходе от языка объектно-ориентированного программирования наследование часто используется для совместного использования кода несколькими классами. Но это решение не всегда лучшее, и в нем есть некоторые проблемы.
Предположим, у вас есть приложение с множеством UIViewController
классов, которые имеют одно и то же поведение - например, все они имеют изображение профиля. Конечно, вы не хотите повторно реализовывать логику изображения профиля - настройку leftBarButtonItem
, открытие и закрытие профиля редактирования ViewController
при нажатии кнопки и т. Д. - во всех контроллерах представлений в вашем приложении.
Решение простое. Просто создайте MainViewController
, подкласс UIViewController
, который реализует все это поведение, а затем сделайте все ваши ViewController
наследуемыми от этого MainViewController
вместо прямого наследования от UIViewController
. Таким образом, все они наследуют эти методы и ведут себя одинаково - нет необходимости каждый раз заново реализовывать все заново.
Но позже, в процессе разработки, вы понимаете, что вам нужен UITableViewController
или UICollectionViewControlle
. К сожалению, вы не можете использовать MainViewController
, потому что он наследует UIViewController
, а неUICollectionViewController
или UITableViewController
.
Как мы можем сделать MainTableViewController
, который реализует те же вещи, что и MainViewController
, но наследует от UITableViewController
вместо UIViewController
? Было бы много дублирования кода, не так ли?
Здесь идет композиция
Конечно, типичный и простой ответ - композиция.
Это означает, что вместо использования наследования UIViewController
содержит или состоит из внутренних классов, обеспечивающих поведение.
В нашем случае мы могли бы представить класс ProfileManager
, который предоставляет все необходимые методы для настройки изображения профиля и взаимодействия с ним. Затем различные UIViewControllers
будут иметь свойство, содержащее ProfileManager
, и смогут использовать его для взаимодействия с кнопкой панели профиля:
Вы видите, как это может усложниться. Вам нужно явно ссылаться на промежуточный объект ProfileManager
каждый раз. В некотором смысле это тоже дублирование, и на его реализацию может уйти больше времени.
Множественное наследование
Еще одна проблема с наследованием заключается в том, что многие объектно-ориентированные языки не допускают множественное наследование - по многим веским причинам, особенно по проблеме ромба.
Это означает, что класс не может наследовать несколько суперклассов.
Допустим, вы Бог и хотите реализовать классы моделей, представляющие живые существа. У вас есть Bird
, Fly
, Human
, Monkey
- как они соотносятся друг с другом? Кто-то может ходить, кто-то летать, кто-то может и то, и другое; некоторые могут говорить, а некоторые нет, некоторые животные, а некоторые нет.
И класс Mockingbird
, и класс Mosquito
могут летать, поэтому мы можем представить суперклассFlyer
, который обеспечивает реализацию метода func fly()
. Но Mockingbird
и Eagle
оба являются птицами, поэтому мы также можем представить Birds
суперкласс, а Mosquito
и Bees
будут подклассами Flies
класса. Итак, Mockinbird
наследуется как от Flyer
, так и от birds
? Это невозможно в Swift (и во многих языках ООП).
Должны ли мы выбирать одно вместо другого? Но если мы создадим Icarus
или, скажем, Superman
подкласс Human
- как насчет реализации метода func fly()
? Мы не можем реализовать его в Human
, потому что не каждый человек умеет летать, но Superman
и Icarus
этот метод тоже понадобится, и мы не хотим его дублировать.
Итак, мы могли бы использовать там композицию, например make class Icarus
быть составленным из свойства var flyingOption: Flyer
. Но писать Icarus.flyingOption.fly()
вместо просто Icarus.fly()
- это некрасиво.
Примеси и черты характера на помощь
Здесь в игру вступает концепция миксинов и трейтов.
- С помощью наследования вы определяете свои классы. Например, каждый
Dog
- этоAnimal
. - С помощью признаков вы определяете, что классы могут. Например, каждый
Animal
можетeat()
, как и люди, птицы и все другие существа.
В то время как наследование позволяет описать, что такое объект, черты позволяют описывать, что объект может делать.
Наследование - это идентичность вашего объекта, но черты определяют, на что способен ваш объект.
Лучше всего то, что класс может принимать несколько Traits
, поскольку он может делать несколько вещей, в то время как он может быть только одним (наследовать только один суперкласс). Это означает, что у нас одна личность, человек, но у нас есть несколько способностей: ходить, говорить, есть, иногда даже летать. Но некоторые другие существа тоже могут это делать, поэтому они не уникальны и специфичны для людей.
Итак, как мы можем сделать это в Swift?
Протоколы - это ответ
Протоколы могут иметь реализацию по умолчанию. В Swift 2.0, когда вы определяете protocol
, вы можете предоставить реализации по умолчанию для некоторых или всех методов этого протокола, используя extension
этого протокола. Вот как это выглядит:
Когда вы создаете класс или структуру, которые соответствуют этому протоколу Flyer
, она получает реализацию метода fly()
бесплатно!
Это по-прежнему реализация по умолчанию - вы можете переопределить этот метод, если вам нужно, но если вы этого не сделаете, у вас все равно будет метод по умолчанию:
Эта функция протоколов с реализацией по умолчанию отлично подходит для многих вещей, включая привнесение концепции «черт» в Swift.
Одна личность, несколько способностей
В чертах замечательно то, что они не зависят от личности объекта, к которому вы их применяете. Им все равно, что это за класс и от чего он наследуется, они просто определяют некоторые функции для этого класса.
Это решает нашу проблему, заключающуюся в том, что Икар одновременно является человеком и может летать, в то время как пересмешник может летать и является птицей.
То, что вы есть, не определяет, что вы можете делать.
Давайте теперь реализуем наши классы моделей, используя трейты. Во-первых, давайте определим различные черты:
Затем давайте дадим им несколько реализаций по умолчанию:
На этом этапе мы по-прежнему будем использовать наследование для определения идентичностей наших существ (что они собой представляют), поэтому давайте создадим несколько родительских классов:
Теперь мы можем определять наших персонажей как по их личности (через наследование), так и по способностям (черты / соответствие протоколу):
Теперь и Icarus
, и IronMan
, и Bee
используют одну и ту же реализацию fly()
, даже если они наследуются от другого суперкласса (Insect
для одного, Human
для другого), и и Паук, и Икар знают, как есть, даже если один человек, а другой - насекомое:
Приключение в пространстве и времени
А теперь давайте представим новую способность / черту космических путешествий:
protocol SpaceTraveler { func travelTo(location: String) }
И дайте ему реализацию по умолчанию:
extension SpaceTraveler { func travelTo(location: String) { print("Let's go to \(location)!") } }
Затем мы можем использовать extensions
Swift, чтобы добавить соответствие протоколу к существующему классу, поэтому давайте добавим эти способности существам, которые мы уже определили:
extension Superman: SpaceTraveler {}
Да, это все, что нужно, чтобы добавить эту способность / черту к существующим классам! Вот так теперь они могут travelTo()
где угодно!
clark.travelTo(location: "Trenzalore") // prints "Let's go to Trenzalore!"
Приглашаем на вечеринку еще людей!
Теперь давайте познакомим еще нескольких людей с этим миксом, на этот раз я хочу использовать больше научно-фантастических существ, чтобы сделать его более нереалистичным и иррациональным, а также сделать больше невозможных способов создания моделей:
У нас тут проблема. Лайка не человек, как и Чуи, а Спок наполовину человек, наполовину вулканец, так что эти определения совершенно неверны.
Вы видите здесь проблему? Мы считали само собой разумеющимся, что Human
и Alien
- личности, и нас снова укусила наследственность. Некоторые классы были вынуждены относиться к определенному типу или унаследовать от родительского класса, хотя на самом деле это не всегда так, особенно в научной фантастике.
Вот почему использование протоколов в Swift и реализаций протоколов по умолчанию может помочь снять ограничения, налагаемые на ваши классы наследованием.
Если бы Human
и Alien
были protocols
вместо classes
, у нас было бы много преимуществ:
- Мы могли бы определить тип
MilleniumFalconPilot
, не заставляя его бытьHuman
, тем самым позволяя Чуи управлять им. - Мы могли бы определить Лайку как
Astronaut
, даже если она неHuman
. - Мы могли бы определить
Spock
какHuman
иAlien
. - В нашем случае мы могли бы даже полностью избавиться от наследования и определить наши типы как
structs
вместоclasses
.struct
не поддерживает наследование, но может соответствовать любому количеству протоколов.
Протоколы везде!
Итак, одно из решений - сделать все протоколом и избавиться от наследования. В конце концов, нам все равно, какие наши существа, героев и существ определяют их способности!
Я включил Swift Playground, который вы можете скачать здесь, который содержит код, показанный в этой статье. Он также демонстрирует еще одну игровую площадку как решение, в котором все сделано из протоколов и структур, без какого-либо наследования. Взглянем!
Конечно, это не означает, что вам нужно любой ценой избавляться от наследства. Наследование по-прежнему полезно и имеет смысл. По-прежнему кажется логичным, что UILabel
является подклассом UIView
. Но это дает вам представление о том, что могут предложить миксины и протоколы с реализациями по умолчанию.
Заключение
Практикуя Swift, вы поймете, что это действительно язык, ориентированный на протоколы, и что использование протокола более распространено и более эффективно в Swift, чем в Objective-C. В конце концов, такие протоколы, как Equatable
, CustomStringConvertible
и любой протокол в -able
в стандартной библиотеке Swift, на самом деле можно рассматривать как миксины!
Даже многие встроенные API Swift используют такие протоколы, как UITableViewDelegate
, UITableViewDataSource
и многие другие.
С помощью протоколов Swift и реализаций протоколов по умолчанию вы можете реализовать миксины и трейты и сделать свой код более гибким.
Подход миксинов и трейтов позволяет вам описывать типы по тому, что они могут делать, а не по тому, чем они являются, и, что более важно, включать возможности в ваши типы. Это все равно, что делать покупки и выбирать возможности для своего типа, независимо от класса, который они наследуют, если таковые имеются.
Возвращаясь к первому примеру - вы можете создать protocol ProfilePictureManager
с реализацией по умолчанию, а затем просто сделать так, чтобы ваши контроллеры представления (будь то UIViewController
, UITableViewController
или еще много чего) соответствовали этому протоколу. Затем он автоматически получит эти возможности и функции от ProfilePictureManager
бесплатно, не беспокоясь о родительском классе UIViewController
!
Спасибо, что прочитали мою статью. Надеюсь, вам понравилось. Есть и другие удивительные вещи, которые мы можем сделать, используя протоколы и расширения протоколов, но эта статья уже достаточно длинная.