面向对象与面向协议

选自猫神的文章 https://onevcat.com/2016/11/pop-cocoa-1/

Swift协议初识

Protocol

Swift 标准库中有 50 多个复杂不一的协议,几乎所有的实际类型都是满足若干协议的。protocol 是 Swift 语言的底座,语言的其他部分正是在这个底座上组织和建立起来的。

所谓协议,就是一组属性和/或方法的定义,而如果某个具体类型想要遵守一个协议,那它需要实现这个协议所定义的所有这些内容。协议实际上做的事情不过是“关于实现的约定”。

面向对象

class Animal {
    var leg: Int { return 2 }
    func eat() {
        print("eat food.")
    }
    func run() {
        print("run with \(leg) legs")
    }
}

class Tiger: Animal {
    override var leg: Int { return 4 }
    override func eat() {
        print("eat meat.")
    }
}

let tiger = Tiger()
tiger.eat() // "eat meat"
tiger.run() // "run with 4 legs"

我们看到 TigerAnimal 共享了一部分代码,这部分代码被封装到了父类中,而除了 Tiger的其他的子类也能够使用 Animal 的这些代码。这其实就是 OOP 的核心思想 - 使用封装和继承,将一系列相关的内容放到一起。

这套理念有一些缺陷。虽然我们努力用这套抽象和继承的方法进行建模,但是实际的事物往往是一系列特质的组合,而不单单是以一脉相承并逐渐扩展的方式构建的。

下面有几个解决方案:

  • Copy & Paste

    这是一个比较糟糕的解决方案,但是演讲现场还是有不少朋友选择了这个方案,特别是在工期很紧,无暇优化的情况下。这诚然可以理解,但是这也是坏代码的开头。我们应该尽量避免这种做法。

  • 引入 BaseViewController

    在一个继承自 UIViewControllerBaseViewController 上添加需要共享的代码,或者干脆在 UIViewController 上添加 extension。看起来这是一个稍微靠谱的做法,但是如果不断这么做,会让所谓的 Base 很快变成垃圾堆。职责不明确,任何东西都能扔进 Base,你完全不知道哪些类走了 Base,而这个“超级类”对代码的影响也会不可预估。

  • 依赖注入

    通过外界传入一个带有 myMethod 的对象,用新的类型来提供这个功能。这是一个稍好的方式,但是引入额外的依赖关系,可能也是我们不太愿意看到的。

  • 多继承

    当然,Swift 是不支持多继承的。不过如果有多继承的话,我们确实可以从多个父类进行继承,并将 myMethod 添加到合适的地方。有一些语言选择了支持多继承 (比如 C++),但是它会带来 OOP 中另一个著名的问题:菱形缺陷。

    菱形缺陷

    上面的例子中,如果我们有多继承,那么 ViewControllerAnotherViewController 的关系可能会是这样的:

    img
    img

    在上面这种拓扑结构中,我们只需要在 ViewController 中实现 myMethod,在 AnotherViewController 中也就可以继承并使用它了。看起来很完美,我们避免了重复。但是多继承有一个无法回避的问题,就是两个父类都实现了同样的方法时,子类该怎么办?我们很难确定应该继承哪一个父类的方法。因为多继承的拓扑结构是一个菱形,所以这个问题又被叫做菱形缺陷 (Diamond Problem)。像是 C++ 这样的语言选择粗暴地将菱形缺陷的问题交给程序员处理,这无疑非常复杂,并且增加了人为错误的可能性。而绝大多数现代语言对多继承这个特性选择避而远之。

动态派发安全性

ViewController *v1 = ...
[v1 myMethod];

AnotherViewController *v2 = ...
[v2 myMethod];

NSObject *v3 = [NSObject new]
// v3 没有实现 `myMethod`

NSArray *array = @[v1, v2, v3];
for (id obj in array) {
    [obj myMethod];
}

// Runtime error:
// unrecognized selector sent to instance blabla

编译依然可以通过,但是显然,程序将在运行时崩溃。Objective-C 是不安全的,编译器默认你知道某个方法确实有实现,这是消息发送的灵活性所必须付出的代价。而在 app 开发看来,用可能的崩溃来换取灵活性,显然这个代价太大了。虽然这不是 OOP 范式的问题,但它确实在 Objective-C 时代给我们带来了切肤之痛。

OOP的三大困境

  • 动态派发安全
  • 横切关注点
  • 菱形缺陷

首先,在 OC 中动态派发让我们承担了在运行时才发现错误的风险,这很有可能是发生在上线产品中的错误。其次,横切关注点让我们难以对对象进行完美的建模,代码的重用也会更加糟糕。

协议拓展和面向协议编程

  • [x] 动态派发安全

    ```swift
    protocol Greetable {
        var name: String { get }
        func greet()
    }
    struct Person: Greetable {
        let name: String
        func greet() {
            print("你好 \(name)")
        }
    }
    struct Cat: Greetable {
        let name: String
        func greet() {
            print("meow~ \(name)")
        }
    }
    //以协议作为类型
    //实现动态派发
    let array: [Greetable] = [
          Person(name: "Wei Wang"), 
          Cat(name: "onevcat")]
    for obj in array {
      obj.greet()
    }
    //❌
    struct Bug: Greetable {
        let name: String
    }
    
    // Compiler Error: 
    // 'Bug' does not conform to protocol 'Greetable'
    // protocol requires function 'greet()'
    ```
    
  • [ ] 横切关注点

    配合协议拓展实现
    
    > 协议拓展 Swift2 实现 WWDC2015提出
    
    ```swift
    protocol P {
        func myMethod()
    }
    //拓展协议 P
    extension P 
      //提供默认实现
        func myMethod() {
            doWork()
        }
    }
    extension ViewController: P { }
    extension AnotherViewController: P { }
    
    viewController.myMethod()
    anotherViewController.myMethod()
    ```
    
    总结:
    
    - 协议定义
      - 提供实现的入口
      - 遵循协议的类型需要对其进行实现
    - 协议扩展
      - 为入口提供默认实现
      - 根据入口提供额外实现
    
  • [ ] 菱形缺陷

    ```swift
    protocol Nameable {
        var name: String { get }
    }
    
    protocol Identifiable {
        var name: String { get }
        var id: Int { get }
    }
    struct Person: Nameable, Identifiable {
        let name: String 
        let id: Int
    }
    
    // `name` 属性同时满足 Nameable 和 Identifiable 的 name
    ```
    
    如果对协议添加了默认实现
    
    ```swift
    extension Nameable {
        var name: String { return "default name" }
    }
    
    struct Person: Nameable, Identifiable {
      //name 使用了 Nameable 的 name 属性
        //let name: String 
        let id: Int
    }
    ```
    
    这样的编译是可以通过的,虽然 `Person` 中没有定义 `name`,但是通过 `Nameable` 的 `name` (因为它是静态派发的),`Person` 依然可以遵守 `Identifiable`。不过,当 `Nameable` 和 `Identifiable` 都有 `name` 的协议扩展的话,就无法编译了:
    
    ```swift
    extension Nameable {
        var name: String { return "default name" }
    }
    
    extension Identifiable {
        var name: String { return "another default name" }
    }
    
    struct Person: Nameable, Identifiable {
        // let name: String 
        let id: Int
    }
    
    // 无法编译,name 属性冲突
    ```
    
    这种情况下,`Person` 无法确定要使用哪个协议扩展中 `name` 的定义。在同时实现两个含有同名元素的协议,**并且**它们都提供了默认扩展时,我们需要在**具体的类型中明确地提供实现**。这里我们将 `Person` 中的 `name` 进行实现就可以了:
    
    ```swift
    struct Person: Nameable, Identifiable {
        let name: String 
        let id: Int
    }
    
    Person(name: "onevcat", id: 123).name // onevcat
    ```
    
    这里的行为看起来和菱形问题很像,但是有一些本质不同。首先,这个问题出现的前提条件是同名元素**以及**同时提供了实现,而协议扩展对于协议本身来说并不是必须的。其次,我们在具体类型中提供的实现一定是安全和确定的。当然,菱形缺陷没有被完全解决,**Swift 还不能很好地处理多个协议的冲突**,这是 Swift 现在的不足。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容