面向协议编程介绍(一切始于协议)

本篇文章翻译自:Introducing Protocol-Oriented Programming in Swift 2)
原作:[Erik kerber]
(https://www.raywenderlich.com/u/kerber) on June 25, 2015


注: 这篇tutorial需要XCode 7, Swift2, 这个秋天会发布测试版。你也可以在 Apple's developer portal下载最新版本。
WWDC 2015, 苹果发布了swift 2,宣布了swift语言第二次重大修订,其中包括了几个新的语言特征,以帮助你提升编码方式。
在这些的新特征中,最令人兴奋的是协议扩展。在swift第一个版本,你可以扩展已经存在的类,结构体,和枚举类型的功能。现在有了swift 2,你也可以扩展协议。
可能刚开始你会觉得这是一个微不足道的特征,但是协议扩展真的能量巨大,且可以转变你的编码方式。在这篇turorial,你会探索创建,使用协议扩展的方法,还有新技术和面向协议编程模式(以下简称:POP)。
你将会看到Swift团队是怎样使用协议扩展以提升swift标准库,和协议扩展怎样影响你的代码。

开始

开始在XCode中创建一个新的Playground, 选择File\New\Playground...,然后命名Playground为SwiftProtocols。你可以选择任何平台,因为这篇tutorial所有的代码跟平台无关。点击Next选择你要保存的路径,然后点击Create.
Playground打开后,我们添加如下代码:

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

     protocol Flyable {
         var airspeedVelocity: Double { get }
     }
}

这里简单的定义了一个拥有2个属性name, 和canFly的Bird协议类型,还有一个Flyable类型,它定义了airspeedVelocity属性。
在pre-protocol时期,你可能会把Flyable当做基类,然后依赖继承去定义Bird还有其他能fly的东西,例如:灰机。请记住:一切始于协议!

定义遵守协议的类型

在Playground下方添加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为计算属性。canFly属性设置为true。
接下来,在playground下面添加以下2个结构体的定义:

struct Penguin: Bird {     
   let name: String     
   let canFly: Bool
}

struct SwiftBird: Bird, Flyable {  
     var name: String { 
        return "swift \(version)" 
     }    
     let version: Double      
     let canFly = true     
     var airspeedVelocity: Double { 
          return 2000.0
     }
}

企鹅是鸟类,但不会飞。哈--- 还好你没有使用继承,让鸟类都能飞。海燕当然有非常快的飞行速度。
你可能已经看到一些冗余。尽管在你的结构中已经有了Flayable的概念,每一种Bird类型还必须声明是否canFly。

用默认实现扩展协议

使用协议扩展,你可以为协议定义默认行为。在Bird协议下添加如下代码:

extension Bird where Self: Flyable {     
   var canFly: Bool { return true }
}

这里定义Bird的一个扩展,当类型也为Flyable时,设置为canFly设置默认行为true。换句话说,任何一个遵守Flyable的Bird类型不必在显示声明。

i wanna everything automatic

swift 1.2 引入了where语法,之后swift 2使用where语法给协议扩展添加约束(Self作用是指,当Bird类型同时也遵守Flyable协议,canFly属性才能生效)
现在从FlappyBird和SwiftBird结构体声明中删除let canFly = true。你会发现playground成功编译了,因为协议扩展帮你处理了协议的要求。

为什么不用基类

协议扩展和其默认实现好像跟其他语言的基类或者抽象类很相似,但是swift具有一下几个关键优势:

  • 因为类型可以遵守多个协议,可以拥有多个协议默认实现的功能。不像其他编程语言支持的类的多继承,协议扩展不会引入额外的状态。
  • 协议可以被类,结构体和枚举类型遵守。基类和继承受限于类类型(class类型)
    换句话说,协议扩展为值类型,而不仅仅是为类类型提供了定义默认行为的能力。
    你已经在结构体中见识过这种行为;接下来在playground添加枚举定义:
enum UnladenSwallow: Bird, Flyable {
    case Africancase
    case Europeancase
    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!")}
    }
}

像其他值类型一样,遵守Bird和Flyable协议,你只需要定义协议要求的属性。因为它遵守这2个协议,所以UnladenSwallow的canFly属性也获得了默认实现。
你真的认为该 tutorial只会包括airspeedVelocity属性,而不会包括Monty Python的引用。(哈,可扩展。。。)

扩展协议

可能协议扩展最普遍的用法就是扩展外部协议了,可能是定义在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 += 1
        }while ( index != self.endIndex)
        return result
    }
}

在CollectionType类型上扩展了一个新的方法skip(:), 它会遍历集合中的每一个元素,过滤掉不符合要求的元素(留下索引值能被参数skip整除对应的元素),然后返回一个过滤后的数组。
CollectionType是一个被诸如swift中数组,字典等类型遵守的协议。这就意味着这个新的行为(skip(
:) 方法)存在你APP中所有的CollectionType类型中。playground中添加如下代码,我们来见证这一行为:

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

这里,你定义了一个包含各种类型鸟类的数组,这些类型上文中你已经定义过。因为数组遵守CollectionType, 那么它就能用skip(_:)方法。

扩展你自己的协议

也许让你兴奋的是你可以像给swift标准库添加新的行为一样,你也可以给自己的协议定义新的行为。
修改Bird协议声明,使之遵守BooleanType协议:

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

遵守BooleanType协议意味着你的类型需要有一个Bool类型的 boolValue属性。那么是不是意味着你必须给现在或者之后的每一个Bird类型都添加这么个属性呢?
当然,我们有更容易的办法———协议扩展。在Bird定义下添加如下代码:

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

这个扩展让canFly属性代表了每一个Bird类型的Boolean 值。playground中添加如下代码,一起来见证奇迹:

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

你会看到"I can fly!" 出现在辅助编辑器上。但是这里你仅是在if 语句中使用了UnladenSwallow。(你可以挖掘更多用法!!!)

对swift标准库的影响

你已经见证过了协议扩展是多么棒--- 你可以使用它自定义和扩展你自己的代码的能力,还有你APP之外的协议(标准库和第三方框架协议)。也许你会好奇,swift团队是怎么使用协议扩展来提升swift标准库的。
swift通过向标准库中添加诸如map, reduce和filter等高阶函数来提升函数式编程范式。这些方法存在不同CollectionType类型中,例如:Array:

["frog", "pants"].map{ $0.length}.reduce(0){$0 + $1}

array调用map会返回另一个array, 新的array调用reduce来归纳结果为9.(map会遍历数组中每一个元素,并且返回由数组长度组成的数组,reduce把新数组中元素长度做加运算)
这种情况下,map和reduce作为swift标准库的一部分被包含在Array中,如果你按下cmd,点击map,你可以看到它怎么定义的:

extension Array : _ArrayType {
         func map(transform: (T) -> U) -> [U]
 }

这里的map函数被定义为Array的一个扩展. swift的函数范式工作范围远不止Array, 它在任意的CollectionType类型上都能工作。所以swift 1.2下是怎么工作的呢?
按下cmd,点击Range的map函数,你将看到以下定义:

extension Range {
      func map(transform: (T) -> U) -> [U]
}

这些表明在swift 1.2中, swift标准库中的各种CollectionType类型需要重新定义map函数。因为尽管Array和Range都是CollectionType,但是结构体不能被子类化,也没有统一的实现。
这并不仅仅是swift标准库做出的细微差别,也对你使用swift类型做出约束。
下面的泛型函数接受一个遵守Flyable协议的CollectionType类型参数,返回airspeedVelocity最大的元素。

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

在swift 1.2中没有协议扩展,这会出现编译错误。map和reduce函数只会存在于预先定义的类型中,不会在任意的CollectionType都会工作。
在swift 2.0中有了协议扩展,Array和Range的map定义如下:

extension CollectionType {  
         func map(@noescape transform: (Self.Generator.Element) -> T) -> [T]
}

尽管你不能看到map源码---至少到swift 2开源(目前已经开源了),ColletionType有map的一个默认实现,而且所有的CollectionType都能免费获得🤗。
在playground下面添加早先提到的泛型函数:

func topSpeed(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.

结果应该无人质疑吧👹(海燕最快)

延伸阅读

你可以在这里下载本tutorial完整playground代码。
你已经见识过POP的威力了,创建你自己的简单协议,然后使用协议扩展来扩展他们。有了默认实现,你可以给已经存在的协议一个统一的实现,有些像基类但是优于基类,因为它还可以应用自爱结构体和枚举类型上。
另外,协议扩展不仅能够勇子啊你自定义的协议上,还可以扩展swift标准库,Cocoa, CocoaTouch中协议的默认行为。
如果想要浏览swift 2.0令人兴奋的新特征,你可以阅读[our "what's new in swift 2.0 article"], 或者 Apple's swift blog
你也可以在苹果开发者入口Protocal Oriented Programming观看WWDC Session深入理解背后的原理。

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

推荐阅读更多精彩内容