Swift! Protocol Oriented

    Всем привет!
    Нет, это не очередной пост в стиле «встречайте Swift и его возможности», а скорее краткий экскурс по практическому применению и тонкостях, где протоколо-ориентированность нового языка от Apple позволяет делать симпатичные и удобные вещи.
    image


    Отдельное приветствие тем, кто заглянул под хабра-кат. В последнее время много приходилось разрабатывать на `Swift`, в тоже время есть большой багаж по объектному С и какое-то желание выразить некоторые вещи кодом, которые я понял намного проще и элегантнее можно реализовать на новом языке.

    Внимание: статья не является подробным обучающим материалам, а представляет практические моменты, которые лично меня впечатлили и я решил ими поделиться, наверное, большинству читателей они покажутся знакомыми, так что кто хочет что-то добавить — просим в комментарии.

    Что ждать в этой статье?


    • Пару вводных предложений (плюс небольшая полезная либка)
    • Декорируем дополнительное поведение класса с Extension (немного кода)
    • Создаем реиспользуемый элемент с помощью протокола и дефолтной имплементации (много кода)
    • Протоколы и enum — может быть удобно (средне кода)

    В чём мощь протоколов?


    Во-первых, как все знают механизм протоколов позволяют реализовывать множественное наследование разнотипных протоколов одним объектом. Наследование нас ограничивает тем, что в цепочке наследников на n-ом шаге нельзя «влить» или «добавить» новое общее с каким-то другим объектом поведение.
    Во-вторых, в Swift есть возможность добавить дефолтную имплементацию (реализацию по умолчанию) для указанного протокола. При этом протокол может иметь несколько реализаций по умолчаний в зависимости от класса или типа объекта, который его наследует.
    В-третьих, протокол можно наследовать от протокола.
    В-четвёртых, протоколы могут быть унаследованы не только классами (Class), но структурами (Struct) и перечислениями (Enum).
    В-пятых, протоколы могу добавлять свойства.
    В-шестых, можно добавлять реализацию по умолчанию и для системных протоколов, а при желании уже переопределять в конкретной классе.
    В завершении добавлю, что протоколы позволяют делать код переиспользуемым в разных классах и структурах. Можно реализовывать частые задачи в них и подключать как декораторы в те файлы, где они необходимы.
    Например, в каждом проекте есть необходимость обработать клик на UIView, чтобы каждый раз не писать лишний код делайте свой класс Tappable(код — тут)
    Лично мне не хватает некоторой конвенции при наследовании протокола, чтобы явно были видны наследуемые методы и свойства (слышал такое есть в Ruby):

    protocol FCActionProtocol {
        var actionButton: UIButton! {get set}
        func showActionView()
    }
    class FCController: FCActionProtocol {
        var actionButton: UIButton! // FCActionProtocol convenience
        func showActionView() {}
    }
    

    Вот хотелось бы, чтобы actionButton и showActionView() подставлялись в автоматически генерируемую область.
    Буду ждать с Swift 3.0

    Декорируем дополнительное поведение класса с Extension


    Итак, от теории к практике: жизненный кейс №1.
    Представим, что у нас есть логика по view cycle у контроллера и логика по передачи модели к view. Внезапно у нас появляется новое расширение контроллера, куда нужно уместить логику по показу почтового клиента. С протоколами это легко:

    class MyViewController: UIViewController { 
    // a lot of code here 
    }
    extension MyViewController: MFMailComposeViewControllerDelegate {
        func showMailController() {
            let mailComposeViewController = configuredMailComposeViewController()
            if MFMailComposeViewController.canSendMail() {
                self.presentViewController(mailComposeViewController, animated: true, completion: nil)
            }
        }
        func configuredMailComposeViewController() -> MFMailComposeViewController {
            let controller = MFMailComposeViewController()
            controller.mailComposeDelegate = self
            return controller // customize and set it here
        }
        // MARK: - MFMailComposeViewControllerDelegate
        func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) {}
    }
    

    Очень радует, что в отличие от obj-c в Swift можно в расширении класса MyViewController указать новые наследуемые протоколы и реализовать их поведение.

    Создаем реиспользуемый элемент с помощью протокола и дефолтной имплементации


    Кейс №2: недавно в приложении на 2-ух экранах была одинаковая кнопка, которая вела к одинаковому сценарию — показу actionSheet с действиями, по одному из которых показывался почтовый клиент. Техническая задача заключалась в том, чтобы реализовать протокол с имплементацией и всей логикой внутри, так чтобы степень сложности его подключения и зависимостей была минимальной. Вот так выглядит код в проекте:

    protocol FCActionProtocol {
        var actionButton: UIButton! {get set}
        var delegateHandler: FCActionProtocolDelegateHandler! {get set}
        mutating func showActionSheet()
        func showMailController()
    }
    class FCActionProtocolDelegateHandler : NSObject, MFMailComposeViewControllerDelegate {
        var delegate: FCActionProtocol!
        init(delegate: FCActionProtocol) {
            super.init()
            self.delegate = delegate
        }
        func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) {
            controller.dismissViewControllerAnimated(true, completion: nil)
        }
    }
    extension FCActionProtocol {
        mutating func showActionSheet() {
            delegateHandler = FCActionProtocolDelegateHandler(delegate: self)
            let actionController = UIAlertController(title: nil, message: nil, preferredStyle: .ActionSheet)
            actionController.addAction(UIAlertAction(title: NSLocalizedString("ActionClear", comment: ""), style: .Default) { (action) in })
            actionController.addAction(UIAlertAction(title: NSLocalizedString("ActionWriteBack", comment: ""), style: .Default) { (action) in
                self.showMailController()
            })
            if let controller = self as? UIViewController {
                controller.presentViewController(actionController, animated: true) {}
            }
        }
        func showMailController() {
            if MFMailComposeViewController.canSendMail() {
                let controller = MFMailComposeViewController()
                controller.mailComposeDelegate = delegateHandler
                (self as! UIViewController).navigationController!.presentViewController(controller, animated: true, completion: nil)
            }
        }
    }
    

    Внимание! Идея кода в том, что есть протокол FCActionProtocol, который включает в себя кнопку, (actionButton) по нажатию на которую происходит показ листа с действиями (showActionSheet). Внутри по клику на элемент листа должен показаться почтовый клиент (showMailController). Для того, чтобы логику и обработку этого вызова не реализовывать в классе, который наследует наш протокол мы делаем дефолтную имплементацию внутри с помощью некоторой абстрактной сущности delegateHandler, которая создается внутри нашего расширения и делегатные методы уже почтового клиента обрабатываются экземпляром класса FCActionProtocolDelegateHandler.

    В результате сложность добавления этого реиспользуемого action-листа заключается в следующем:

    class FCMyController: FCActionProtocol {
        var actionButton: UIButton! // convenience FCActionProtocol
        var delegateHandler: FCActionProtocolDelegateHandler! // convenience FCActionProtocol
    }
    

    Вся логика внутри. Нам нужно только проинициализировать и добавить кнопку. На мой взгляд, получилось красиво и лаконично.

    Протоколы и enum — может быть удобно


    Жизненный кейс №3: наша команда делала сервис по продаже авиабилетов онлайн. Мобильный клиент тесно общается с сервером и есть разные сценарии при которых делается обращения к API. Разделим их условно на поиск, бронирование билета и оплату. В каждом из этих процессов может произойти ошибка (на стороне сервера, клиента, протокола общения, валидации данных и так далее). Если при бронировании или поиске 500-ая с сервера еще не несёт ничего страшного, то, например, при оплате данные с внутреннего сервера могли уже уйти в платежный шлюз и нельзя клиенту просто показать ошибку, в то время как его деньги могли быть списаны с банковской карты.
    Здесь протоколы могу позволить создать достаточно изящный код:

    protocol Critical {
        func criticalStatus() -> (critical: Bool, message: String)
    }
    enum Error {
        case Search(code: Int)
        case Booking(code: Int)
        case Payment(code: Int)
    }
    extension Error : Critical {
        func criticalStatus() -> (critical: Bool, message: String) {
            switch self {
            case .Payment(let code) where code == 500:
                return (true, "Please contact us, because your payment could proceed")
            default:
                return (false, "Something went wrong. Please try later.")
            }
        }
    }
    

    Теперь дёргаем наш код и оцениваем насколько всё плохо:

    let error = Error.Payment(code: 500)
    if error.criticalStatus().critical {
        print("callcenter will solve it")
    }
    

    Жалко, что в реальности проект представлял огромный пласт objective-c с кучей хаков для совместимости с Swift.
    Надеюсь, что следующие проекты можно будет реализовывать, используя все возможности языка.

    P.s. Надеюсь, кто-то начинающий заинтересуется Swift-ом и подходом разработки с использованием протоколов. Может быть кто-то из middle отметит для себя пару приёмов, которые он не использовал. А сеньоры не будут критиковать и поделятся в комментариях парой своих секретов и наработок. Всем спасибо.
    • +5
    • 17,2k
    • 5
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 5
    • 0
      Очень радует, что в отличие от obj-c в Swift можно в расширении класса MyViewController указать новые наследуемые протоколы и реализовать их поведение.

      Учитывая, что эти расширения, суть, симбиоз протоколов и категорий, все это в obj-c можно делать. Протоколы наследуют протоколы. Категории наследуют и реализуют протоколы.
      Выглядит забавно и любопытно, но, честно, никогда не сталкивался с необходимостью расширений, которые не привязаны к какому-то типу. Больше похоже на синтаксический сахар для helper-функций и методов. Называть это прям какой-то новой парадигмой protocol-oriented язык не поворачивается
      • 0
        В obj-c не сделаешь миксинов, как вы и описали — протоколов не привязанных к типу.

        Зачем они могут быть нужны?
        Самый явный пример — уточнение дженериков.
        Если не вдаваться в библиотеки (мы, например, очень любим писать расширения к generic сущности SignalProducer из ReactiveCocoa) — то в коде встречались расширения на Optional на конкретные типы.
        Пример — единообразный анврап Optional объекта (замена конструкции value = optional ?? fallbackValue на какую-то единообразную optional.unwrap, которая предпочтительнее, чем unwrap(optional)). Для того чтобы его написать — нужно уметь строить fallbackValue — что можно сделать только для какого-то набора типов.
      • 0
        Я дико извиняюсь, но всё же рискну спросить: а что в данном контексте есть протокол?
        • 0
          Может быть это как интерфейс из ООП.
          • +1
            Собственно без контекстов, протоколом считаю протокол, как его описывает Apple:
            A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol.

            Но в принципе его можно рассматривать как некоторый миксин, так как по факту (как показано в статье, например, в примере 2) можно «вливать» новых функционал в существующий класс и делать это неоднократно.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.