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 = Помилка.Payment(code: 500)
if error.criticalStatus().critical {
print("callcenter will solve it")
}

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

P. s. Сподіваюся, хто-то початківець зацікавиться Swift-му і підходом розробки з використанням протоколів. Може бути хтось із middle відзначить для себе пару прийомів, які він не використав. А сеньйори не будуть критикувати і поділяться в коментарях парою своїх секретів і напрацювань. Всім дякую.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.