Зачем нужны протоколы и когда их использовать

Вступление

У меня было много собеседований со стартапами и крупными компаниями, и все они задавали мне такие вопросы:

  • В чем разница между протоколами и классами?
  • Почему бы не использовать суперклассы вместо протоколов?

Эта статья даст вам ответ на эти вопросы и может познакомить вас с новым аспектом программирования на 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!

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

Ссылки и ресурсы