[译]Swift2的面向接口编程(Protocol-Oriented Programming)

原文来自Raywenderlich,作者Eric Kerber,原文链接:
https://www.raywenderlich.com/109156/introducing-protocol-oriented-programming-in-swift-2

Note: 本教程需要Xcode7以及Swift2,可以在Apple官方网站下载(译者注:原文发布是Swift2还处于beta阶段)。

在WWDC2015时,Apple发布了Swift语言的第二个大的版本更新——Swift 2。新版本包含了很多新的语言特性(译者注:中文改动可以看这里,不过推荐阅读英文版改动日志),让Swift在使用的时候更加得心应手。
  众多改动之中,最引人注意的就是protocol extensions。在Swift第一版中,我们可以通过extension来为已有的classstructenum拓展功能。而在Swift 2中,我们也可以为protocol添加extension。
  可能一开始看上去这个新特性并不起眼,实际上protocol extensions非常强大,以至于可以改变Swift之前的某些编程思想。在本教程中,我们会展示如何创建并使用protocol extensions,以及随之而来的一些新技巧和面向接口编程(Protocol-oriented programming,以下简称POP)模式对代码的影响。
  你将会了解Swift团队是如何使用protocol extensions来改善Swift标准库,以及如何使用这些技巧来改善自己的代码。

准备工作

首先,创建一个新的playground。在Xcode中,选择File\New\Playground...,然后将playground命名为SwiftProtocols。platform可以选择任意选项,之后我们要构建的代码都是平台无关的。点击Next选择文件路径,最后点击Create
  新的playground打开之后,加入如下代码:

protocol Bird {
  var name: String { get }
  var canFly: Bool { get }
}

protocol Flyable {
  var airspeedVelocity: Double { get }
}

以上代码定义了一个简单的protocol——Bird。它有namecanFly两个属性。还有一个protocol叫做Flyable,定义了属性airspeedVelocity
之前,你或许会把Flyable定义成一个基类(base class),然后利用继承的方法来定义Birds以及其它“会飞”的类型,例如飞机(译者注:Flyable意为飞行物,Birds就是鸟)。而从现在开始,所有的东西都开始被定义为protocol了!
  当我们开始定义具体的类型的时候,你就会发现,这一切会让整个系统变得多么灵活。

定义Protocol-conforming类型

将下面定义struct的代码添加到playground中:

struct FlappyBird: Bird, Flyable {
  let name: String
  let flappyAmplitude: Double
  let flappyFrequency: Double
  let canFly = true

  var airspeedVelocity: Double {
    return 3 * flappyFrequency * flappyAmplitude
  }
}

这里定义了一个新的struct类型——FlappyBird。它同时满足BirdFlyable两个protocol。airspeedVelocity作为一个computed property,由flappyFrequencyflappyAmplitude计算得出。作为“会飞的鸟”,FlappyBird将canFly设置为true。
  接下来,把下面定义两个struct的代码加到playground中(译者注:Penguin是企鹅,SwiftBird可以理解为速度很快的鸟):

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 }
}

PenguinBird是is-a关系,但并不能飞。Aha——幸亏没有使用继承的方式,否则所有的Bird都会被定义为Flyable。对于SwiftBird,自然而然的,它速度很快,飞行速度的数值也很高。
  然而,或许你已经注意到了,目前的实现还是有一些冗余存在。每一种Bird类型必须声明canFly属性,不管它到底会不会飞。我们的系统中已经有了Flyable的概念,却并没能很好的利用起来。

用默认实现来拓展Protocols

利用protocol extensions,我们可以定义protocol的默认行为。添加下面的代码到playground中:

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

这段代码定义了一个Bird的extension, 并设置了canFly属性的默认行为。当实现Bird protocol的对象同时实现了Flyable protocol的时候,让canFly的值总是返回true。换句话说,“会飞的鸟”终于不用再显式的声明自己“会飞”了!


  Swift 1.2 引入了与if-let关联的where语法,Swift 2则把where进一步用于了有条件的拓展protocol。
  删除FlappyBirdSwiftBird定义里面的let canFly = true。你会发现playground依然成功build,因为protocol extension帮你处理了冗余的部分。

为什么不使用基类(Base Classes)?

Protocol extensions和默认行为(Default Behavior)可能看起来和使用基类很相似。更确切一点,很像其它一些语言中的abstract class。但在Swift中,protocol extensions主要有如下一些便利:

  • 因为类型可以同时满足超过一个protocol,我们可以用多个不同的protocol的默认行为来构建这个类型对象。不同于某些语言支持的多重继承(multiple inheritance),protocol extensions并不会引入额外的状态。
  • Protocols可以用于类,结构体和枚举类型(classes,structs and enums),而基类和继承只能用于类类型(class types)。

换句话说,protocol extensions为值类型(value types)提供了定义默认行为的方法。
  展示了struct类型的例子之后,让我们把下面的enum定义加到playground中:

 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满足两个protocol即可(译者注:UnladenSwallow当成是一种燕子就行)。因为它同时满足BirdFlyable,所以直接拥有canFly的默认实现。
  你难道觉得这个用了airspeedVelocity的教程不会提到Monty Python么?:](译者注:貌似是作者用了一个梗,Monty Python是国外一个著名的喜剧团体)。

Protocols的扩展

大概protocol extensions最常见的用途就是用来扩展外部的protocols了,往往要么是来自Swift标准库,要么是来自第三方库。
把下面的代码加入playground:

 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的extension,其中包含了一个新的skip(_:)方法。这个方法会遍历collection中的内容并每次“跳过”特定个数的元素,最终返回那些没有被“跳过”的元素的collection。
  CollectionType是一个protocol。Swift中一些例如数组(arrays)和字典(Dictionaries)都实现了这个protocol。这意味着现在skip(_:)这个新方法适用于所有的CollectionType

扩展自定义的Protocols

给Swift标准库添加新行为让人兴奋。与此同时,我们也可以利用标准库的protocol extensions给自定义的类型定义默认行为。
  修改Bird protocol使得它满足BooleanType protocol:

protocol Bird: BooleanType {

满足BooleanType意味着这个类型要拥有一个booleanValue属性,让它能够像一个布尔值一样被使用。这是不是表示我们要给每个Bird类型都添加这个属性呢?
  当然不需要,有更简单的方法。添加如下代码到Bird的定义中:

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

这个extension会让canFly属性代表每个Bird类型的布尔值。
  为了测试一下效果,添加如下代码到playground的末尾:

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

你应该看到“I can fly”出现在了辅助编辑器上。但更加值得注意的是,你刚刚在if条件语句中使用了African Unladen Swallow!

对Swift标准库的影响

你已经了解了protocol extensions在拓展自定义以及外部代码功能方面是多么好用。而Swift团队用protocol extensions来改善Swift标准库的方法可能会更加让你感到惊奇。
  Swift相当提倡函数式编程,在标准库中引入了很多范例,包括mapreducefilter等。这些方法被应用在很多CollectionType中,比如Array

// 计算数组中有多少个字符
["frog", "pants"].map { $0.length }.reduce(0) { $0 + $1 } // 返回 9

对数组调用map方法会返回另一个数组,而reduce则将结果归约成一个最终值9。
  在这个例子中,mapreduce都是Swift标准库的一部分。如果你按下cmd同时点击map,你可以看到具体的方法定义。
  在Swift 1.2中,定义看起来是这样的:

// Swift 1.2
extension Array : _ArrayType {
  /// 返回一个 `Array`, 其中包含调用的结果
  /// `transform(x)` 作用于每个`self`的`x`元素
  func map<U>(transform: (T) -> U) -> [U]
}

这表明在Swift 1.2中,map需要在每个Swift标准库的CollectionType中被重定义!这是因为即使ArrayRange都是CollectionType,结构体(struct)不能有子类,也不能有共享的方法实现。
  这不仅仅是Swift标准库的实现细节,这为我们使用Swift类型增加了限制。
下面的泛型方法接受一个FlyableCollectionType,返回其中airspeedVelocity最高的元素。

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

在Swift 1.2中,没有protocol extensions,这实际上会带来编译错误。mapreduce只存在于定义好的具体类型当中,对于任意的CollectionType是没有效果的。
  在Swift 2中,得益于protocol extensions,map的定义变成了如下:

// Swift 2.0
extension CollectionType {
  /// 返回一个 `Array`,包含把 `transform`映射到
  /// `self`上的结果.
  ///
  /// - 复杂度: O(N).
  func map<T>(@noescape transform: (Self.Generator.Element) -> T) -> [T]
}

即使你在Swift开源之前暂时不能看到源代码(译者注:现在已经开源),所有的CollectionType现在都拥有了map的默认实现!
  将下面之前提到过的泛型方法加入到playground中:

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

mapreduce方法现在可以用于你的Flyable集合实例了。把下面的代码加入playground,你现在终于可以回答它们之中谁是最快的了:

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

topSpeed(flyingBirds) // 2000.0

结果毫无疑问,不是么?:]

下一步该做什么

可以在这里下载本教程完整的playground代码。
  通过搭建简单的protocols并用protocol extensions拓展它们,你已经见识过了POP的强大之处。使用默认实现,你可以让已有的protocols拥有简明且自动化的行为,类似基类但胜在能应用于struct和enum类型。
  更进一步的,protocol extensions不仅能扩展自定义的protocols,还可以扩展Swift标准库,Cocoa以及Cocoa Touch中的protocols。
  想要对Swift 2的新特性进行总览的话,可以阅读我们的what's new in Swift 2.0,或者移步官方博客Apple’s Swift blog
  Apple’s developer portal上有个很棒的WWDC视频推荐:Protocol Oriented Programming。想要深度了解POP理论的同学可以看看。
  如果还有什么问题,欢迎在原文以及这里的评论区进行讨论。

写在最后:本文中一些不影响文章理解的专业名词没有进行翻译,比如protocol和extensions,我尽量避免使用诸如“协议拓展”之类的字眼,一是避免和大家头脑中的中文版本产生偏差,二是对于这些词汇尽量标准化比较好。POP是翻译成“面向接口编程”还是“面向协议编程”纠结了很久,还是感觉“接口”用起来更容易被理解。第一次翻译技术文章,希望大家能提出一些建设性意见:-)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容