Всем привет!
Нет, это не очередной пост в стиле «встречайте Swift и его возможности», а скорее краткий экскурс по практическому применению и тонкостях, где протоколо-ориентированность нового языка от Apple позволяет делать симпатичные и удобные вещи.
Отдельное приветствие тем, кто заглянул под хабра-кат. В последнее время много приходилось разрабатывать на `Swift`, в тоже время есть большой багаж по объектному С и какое-то желание выразить некоторые вещи кодом, которые я понял намного проще и элегантнее можно реализовать на новом языке.
Внимание: статья не является подробным обучающим материалам, а представляет практические моменты, которые лично меня впечатлили и я решил ими поделиться, наверное, большинству читателей они покажутся знакомыми, так что кто хочет что-то добавить — просим в комментарии.
Во-первых, как все знают механизм протоколов позволяют реализовывать множественное наследование разнотипных протоколов одним объектом. Наследование нас ограничивает тем, что в цепочке наследников на n-ом шаге нельзя «влить» или «добавить» новое общее с каким-то другим объектом поведение.
Во-вторых, в Swift есть возможность добавить дефолтную имплементацию (реализацию по умолчанию) для указанного протокола. При этом протокол может иметь несколько реализаций по умолчаний в зависимости от класса или типа объекта, который его наследует.
В-третьих, протокол можно наследовать от протокола.
В-четвёртых, протоколы могут быть унаследованы не только классами (Class), но структурами (Struct) и перечислениями (Enum).
В-пятых, протоколы могу добавлять свойства.
В-шестых, можно добавлять реализацию по умолчанию и для системных протоколов, а при желании уже переопределять в конкретной классе.
В завершении добавлю, что протоколы позволяют делать код переиспользуемым в разных классах и структурах. Можно реализовывать частые задачи в них и подключать как декораторы в те файлы, где они необходимы.
Например, в каждом проекте есть необходимость обработать клик на UIView, чтобы каждый раз не писать лишний код делайте свой класс Tappable(код — тут)
Лично мне не хватает некоторой конвенции при наследовании протокола, чтобы явно были видны наследуемые методы и свойства (слышал такое есть в Ruby):
Вот хотелось бы, чтобы actionButton и showActionView() подставлялись в автоматически генерируемую область.
Буду ждать с Swift 3.0
Итак, от теории к практике: жизненный кейс №1.
Представим, что у нас есть логика по view cycle у контроллера и логика по передачи модели к view. Внезапно у нас появляется новое расширение контроллера, куда нужно уместить логику по показу почтового клиента. С протоколами это легко:
Очень радует, что в отличие от obj-c в Swift можно в расширении класса MyViewController указать новые наследуемые протоколы и реализовать их поведение.
Кейс №2: недавно в приложении на 2-ух экранах была одинаковая кнопка, которая вела к одинаковому сценарию — показу actionSheet с действиями, по одному из которых показывался почтовый клиент. Техническая задача заключалась в том, чтобы реализовать протокол с имплементацией и всей логикой внутри, так чтобы степень сложности его подключения и зависимостей была минимальной. Вот так выглядит код в проекте:
Внимание! Идея кода в том, что есть протокол FCActionProtocol, который включает в себя кнопку, (actionButton) по нажатию на которую происходит показ листа с действиями (showActionSheet). Внутри по клику на элемент листа должен показаться почтовый клиент (showMailController). Для того, чтобы логику и обработку этого вызова не реализовывать в классе, который наследует наш протокол мы делаем дефолтную имплементацию внутри с помощью некоторой абстрактной сущности delegateHandler, которая создается внутри нашего расширения и делегатные методы уже почтового клиента обрабатываются экземпляром класса FCActionProtocolDelegateHandler.
В результате сложность добавления этого реиспользуемого action-листа заключается в следующем:
Вся логика внутри. Нам нужно только проинициализировать и добавить кнопку. На мой взгляд, получилось красиво и лаконично.
Жизненный кейс №3: наша команда делала сервис по продаже авиабилетов онлайн. Мобильный клиент тесно общается с сервером и есть разные сценарии при которых делается обращения к API. Разделим их условно на поиск, бронирование билета и оплату. В каждом из этих процессов может произойти ошибка (на стороне сервера, клиента, протокола общения, валидации данных и так далее). Если при бронировании или поиске 500-ая с сервера еще не несёт ничего страшного, то, например, при оплате данные с внутреннего сервера могли уже уйти в платежный шлюз и нельзя клиенту просто показать ошибку, в то время как его деньги могли быть списаны с банковской карты.
Здесь протоколы могу позволить создать достаточно изящный код:
Теперь дёргаем наш код и оцениваем насколько всё плохо:
Жалко, что в реальности проект представлял огромный пласт objective-c с кучей хаков для совместимости с Swift.
Надеюсь, что следующие проекты можно будет реализовывать, используя все возможности языка.
P.s. Надеюсь, кто-то начинающий заинтересуется Swift-ом и подходом разработки с использованием протоколов. Может быть кто-то из middle отметит для себя пару приёмов, которые он не использовал. А сеньоры не будут критиковать и поделятся в комментариях парой своих секретов и наработок. Всем спасибо.
Нет, это не очередной пост в стиле «встречайте Swift и его возможности», а скорее краткий экскурс по практическому применению и тонкостях, где протоколо-ориентированность нового языка от Apple позволяет делать симпатичные и удобные вещи.
Отдельное приветствие тем, кто заглянул под хабра-кат. В последнее время много приходилось разрабатывать на `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)
Sirion
05.12.2015 18:20Я дико извиняюсь, но всё же рискну спросить: а что в данном контексте есть протокол?
katleta
06.12.2015 11:43+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) можно «вливать» новых функционал в существующий класс и делать это неоднократно.
creker
Учитывая, что эти расширения, суть, симбиоз протоколов и категорий, все это в obj-c можно делать. Протоколы наследуют протоколы. Категории наследуют и реализуют протоколы.
Выглядит забавно и любопытно, но, честно, никогда не сталкивался с необходимостью расширений, которые не привязаны к какому-то типу. Больше похоже на синтаксический сахар для helper-функций и методов. Называть это прям какой-то новой парадигмой protocol-oriented язык не поворачивается
i_user
В obj-c не сделаешь миксинов, как вы и описали — протоколов не привязанных к типу.
Зачем они могут быть нужны?
Самый явный пример — уточнение дженериков.
Если не вдаваться в библиотеки (мы, например, очень любим писать расширения к generic сущности SignalProducer из ReactiveCocoa) — то в коде встречались расширения на Optional на конкретные типы.
Пример — единообразный анврап Optional объекта (замена конструкции value = optional ?? fallbackValue на какую-то единообразную optional.unwrap, которая предпочтительнее, чем unwrap(optional)). Для того чтобы его написать — нужно уметь строить fallbackValue — что можно сделать только для какого-то набора типов.