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 отметит для себя пару приёмов, которые он не использовал. А сеньоры не будут критиковать и поделятся в комментариях парой своих секретов и наработок. Всем спасибо.

ссылка на оригинал статьи http://habrahabr.ru/post/272005/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *