Знакомство с протоколо-ориентированным программированием в Swift 2

Протоколо-ориентированным программированием в Swift 2

Заметка

Примечание: Это руководство требует Xcode 7 и Swift 2.

На WWDC 2015 компания Apple анонсировала вторую главную доработку языка Swift - обновление до Swift 2. Новая версия включает в себя несколько новых языковых функций для улучшения способа написания кода.

Среди наиболее захватывающих особенностей - расширение протокола. В первой версии Swift, была возможность расширить функциональность существующих типов class, struct и enum. Теперь, в Swift 2, вы можете также расширить и protocol.

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

Вы также увидите, как команда Swift использовала расширения протокола для улучшения стандартной библиотеки языка, и как это теперь влияет на код, который вы пишете.

Итак, поехали

Начните с создания нового плейграунда. В Xcode, выберите File\New\Playground ... и назовите плейграунд SwiftProtocols. Вы можете выбрать любой плейграунд, так как весь код в этом уроке является platform-agnostic. Выберите Next для выбора места сохранения и, наконец, нажмите кнопку Create.

После того, как ваш новый плейграунд создан, добавьте следующий код:

protocol Bird {
  var name: String { get }
  var canFly: Bool { get }
}
 
protocol Flyable {
  var airspeedVelocity: Double { get }
}

Мы обозначаем простой протокол Bird со свойствами name и canFly, а также Flyable протокол, который определяет airspeedVelocity. В допротокольном мире, вы, возможно, начали с Flyable в качестве базового класса, а затем основываясь на наследовании, определили Bird как объект, который может летать, как например, самолеты. Обратите внимание, что здесь все начинает определяться как протокол!

Вы увидите, что определяя фактические типы в дальнейшем, вы сделаете всю систему более гибкой.

Определение соответствующих протоколу типов

Добавьте следующее определение struct в нижней части вашего плейграунда:

struct FlappyBird: Bird, Flyable {
  let name: String
  let flappyAmplitude: Double
  let flappyFrequency: Double
  let canFly = true
 
  var airspeedVelocity: Double {
    return 3 * flappyFrequency * flappyAmplitude
  }
}

Это определит новую структуру FlappyBird, которая соответствует и протоколу Bird, и протоколу Flyable. Его airspeedVelocity рассчитывается как функция flappyFrequency и flappyAmplitude. Будучи порхающей (flappy), возвращается true для canFly. :]

Затем добавьте следующие два определения структуры в нижней части плейграунда:

struct Penguin: Bird {
  let name: String
  let canFly = false
}
 
struct SwiftBird: Bird, Flyable {
  var name: String { return "Swift \(version)" }
  let version: Double
  let canFly = true
 
  // Swift is FAST!
  var airspeedVelocity: Double { return 2000.0 }
}

Penguin (пингвин) - это Bird (птица), но не может летать. А-ха! Это хорошо, что вы не выбрали метод наследования, и определили, что все птицы летают! SwiftBird, конечно, имеет очень большую скорость полета!

Тем не менее уже можно увидеть некоторую избыточность. Каждый тип Bird должен объявить либо он canFly или нет, несмотря на то, что уже есть понятие Flyable в вашей системе.

Расширение протоколов с дефолтной реализацией

С расширением протоколов вы можете определить дефолтное поведение для протокола. Добавьте следующее определение протокола Bird:

extension Bird where Self: Flyable {
  // Flyable birds can fly!
  var canFly: Bool { return true }
}

Это определяет расширение Bird, которое устанавливает поведение для canFly с возвратом true всякий раз, когда тип также Flyable. Другими словами, любая птица Flyable не нуждается больше в том, чтобы ее явно так обозначать!

Swift 1.2 вводит поправку where для опциональной привязки if-let, и Swift 2 распространяет привязку для условного расширения протокола.

Удалите

let canFly = true

из FlappyBird и из описания структуры SwiftBird. Вы увидите, что плейграунд успешно создается, так как расширение протокола теперь обрабатывает это требование для вас.

 

Почему бы не базовые классы?

Расширения протокола и реализации по умолчанию могут показаться схожими с использованием базового класса или даже абстрактных классов в других языках, но они предлагают несколько ключевых преимуществ в Swift:

  1. Поскольку типы могут соответствовать более чем одному протоколу, они также могут быть «украшены» дефолтным поведением из нескольких протоколов. В отличие от множественного наследования классов, которые поддерживают некоторые языки программирования, расширения протоколов не привносят дополнительного состояния.
  2. Протоколы могут быть заимствованы классами, структурами и перечислениями. Базовые классы и наследование ограничены до классовых типов.

Другими словами, расширения протокола обеспечивают возможность определить дефолтное поведение для типов значений и не только классам.

Вы уже видели это в действии со структурой, теперь добавьте следующее определение перечисления в конец плейграунда:

enum UnladenSwallow: Bird, Flyable {
  case African
  case European
  case Unknown
 
  var name: String {
    switch self {
      case .African:
        return "African"
      case .European:
        return "European"
      case .Unknown:
        return "What do you mean? African or European?"
    }
  }
 
  var airspeedVelocity: Double {
    switch self {
      case .African:
        return 10.0
      case .European:
        return 9.9
      case .Unknown:
        fatalError("You are thrown from the bridge of death!")
    }
  }
}

Как и в любом другом типе значения, все, что вам нужно сделать, это определить корректные свойства, так UnladenSwallow соответствует двум протоколам. Так как он соответствует Bird и Flyable, он также получает реализацию по умолчанию для canFly!

Расширение протоколов

Возможно, самый распространенный способ использования расширений протоколов - это расширение внешних протоколов, либо тех, которые определены в стандартной библиотеке Swift или фрейворками третьей стороны.

Добавьте следующее в нижней части плейграунда:

extension CollectionType {
  func skip(skip: Int) -> [Generator.Element] {
    guard skip != 0 else { return [] }
 
    var index = self.startIndex
    var result: [Generator.Element] = []
    var i = 0
    repeat {
      if i % skip == 0 {
        result.append(self[index])
      }
      index = index.successor()
      i++
    } while (index != self.endIndex)
 
    return result
  }
}

Это определяет расширение CollectionType, который определяет новый skip(_:) метод, который «пропускает» каждый skip элемент в коллекции и возвращает в массив только не пропущенных элементов.

CollectionType это протокол, под который подписаны такие типы как массивы и словари в Swift. Это означает, что это новое поведение в настоящее время существует в каждом CollectionType в вашем приложении! Добавьте следующий код в нижнюю часть вашего плейграунда, чтобы увидеть его в действии:

let bunchaBirds: [Bird] =
  [UnladenSwallow.African,
   UnladenSwallow.European,
   UnladenSwallow.Unknown,
   Penguin(name: "King Penguin"),
   SwiftBird(version: 2.0),
   FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0)]
 
bunchaBirds.skip(3)

Здесь вы выбираете массив птиц, включая многие типы, которые вы уже определяли. Так массивы соответствуют CollectionType, это означает, что skip(_:) доступен в них тоже.

Расширение собственных протоколов

Возможно, столь же захватывающим, как и добавление новых моделей поведения, на основе протоколов из стандартной библиотеки Swift, станет для вас и определение дефолтного поведения.

Измените описание протокола Bird для соответствия протоколу BooleanType:

protocol Bird: BooleanType {

Соответствие с BooleanType означает, что ваш тип должен иметь свойство boolValue, поэтому он действует как Boolean (логически). Значит ли это, что теперь вам нужно добавлять этот свойство в каждый текущий и будущий тип Bird?

Конечно, есть и более простой способ с расширениями протокола. Добавьте код под определение Bird:

extension BooleanType where Self: Bird {
  var boolValue: Bool {
    return self.canFly
  }
}

Это расширение заставит свойство canFly отображать логическое значение каждого Bird типа.

Чтобы попробовать это, добавьте следующие строчки в в конец плейграунда:

if UnladenSwallow.African {
  print("I can fly!")
} else {
  print("Guess I’ll just sit here :[")
}

Вы должны увидеть, что появилась надпись ”I can fly!” ("Я могу летать!") на вспомогательном редакторе. Примечательно, что вы просто использовали African Unladen Swallow в операторе if!

Влияние на стандартную библиотеку Swift

Вы убедились, что расширения протокола являются отличным способом для настройки и расширения возможностей вашего кода, а также протоколов, определенных вне вашего приложения. Что может удивить вас, так это как команда Swift использовала расширения протокола для улучшения способа написания стандартной библиотеки Swift.

Swift помогает продвигать функциональные парадигмы программирования, включая такие методы как: map, reduce и filter, в стандартную библиотеку. Эти методы встречаются у различных представителей CollectionType, таких как Array:

// Counts the number of characters in the array
["frog", "pants"].map { $0.length }.reduce(0) { $0 + $1 } // returns 9

Вызывая map, мы возвращаем еще один массив, на котором, где используем reduce, что уменьшает результат до конечного значения 9.

В этом случае, map и reduce включены в Array в качестве части стандартной библиотеки Swift. Если вы Cmd-Click на map, вы можете видеть, как она определена.

В Swift 1.2 вы бы увидели вот такое определение:

// Swift 1.2
extension Array : _ArrayType {
  /// Return an `Array` containing the results of calling
  /// `transform(x)` on each element `x` of `self`
  func map<U>(transform: (T) -> U) -> [U]
}

Функция map определяется здесь как расширение Array. Однако функциональные функции Swift работают больше, чем просто для Array, они так же должны быть для любого представителя CollectionType, так как же Swift 1.2 проделывает эту работу?

Если вы вызовите map для Range и Cmd-Click на map оттуда, то вы увидите следующее:

// Swift 1.2
extension Range {
  /// Return an array containing the results of calling
  /// `transform(x)` on each element `x` of `self`.
  func map<U>(transform: (T) -> U) -> [U]
}

Оказывается, что в Swift 1.2 map должна быть предопределена для каждого CollectionType в стандартной библиотеке Swift! Это потому, что, хотя Array и Range оба принадлежат к CollectionType, а структуры не могут быть подклассифицированы и не могут иметь общих реализаций.

Это не просто один нюанс, о том как именно устроена стандартная библиотека Swift, это накладывает ограничения на то, что вы можете сделать с Swift типами.

Общая ниже функция берет CollectionType из Flyable и возвращает элемент с наибольшим airspeedVelocity:

func topSpeed<T: CollectionType where T.GeneratorType: Flyable>(collection: T) -> Double {
  collection.map { $0.airspeedVelocity }.reduce { max($0, $1)}
}

Функции map и reduce существуют только на наборе предопределенных типов и не будут работать на любом произвольном CollectionType.

На Swift 2.0 и с расширениями протокола, определение для map в обоих Array и Range выглядит следующим образом:

// Swift 2.0
extension CollectionType {
  /// Return an `Array` containing the results of mapping `transform`
  /// over `self`.
  ///
  /// - Complexity: O(N).
  func map<T>(@noescape transform: (Self.Generator.Element) -> T) -> [T]
}

И хотя вы не можете увидеть исходный код map - по крайней мере, до тех пор пока Swift 2 не станет с открытым исходным кодом (open sourced )! - CollectionType теперь имеет дефолтную реализацию map, которую все виды CollectionType получают бесплатно!

Добавьте общую функцию, упомянутую выше, к нижней части плейграунда:

func topSpeed<T: CollectionType where T.Generator.Element == Flyable>(c: T) -> Double {
  return c.map { $0.airspeedVelocity }.reduce(0) { max($0, $1) }
}

Методы map и reduce доступны для вашей коллекции экземпляров Flyable. Теперь вы можете, наконец, ответить на вопрос о том, кто же из них самый быстрый, для добавления последующего кода в нижней части плейграунда:

let flyingBirds: [Flyable] = 
  [UnladenSwallow.African,
  UnladenSwallow.European,
  SwiftBird(version: 2.0)]
 
topSpeed(flyingBirds) // 2000.0

Как будто были сомнения! :]

Источник: http://www.raywenderlich.com/109156/introducing-protocol-oriented-programming-in-swift-2