不要用子类!Swift的核心是面向协议

本文转自:http://www.cocoachina.com/swift/20150803/12881.html
原作者:Hector Matos
原发表日期:2015-07-13

<b>本文的代码示例改用swift3.0,原文使用的是swift2.0</b>

<h2>Swift的核心</h2>
我们可以通过等式的传递性来理解swift:
Swift 的核心是面向协议的编程。
面向协议的编程的核心是抽象和简化。
所有swift的核心就是抽象和简化

<small>
你可能对我的标题感到诧异。我并不是说子类没有价值,尤其在使用单一继承的情况下,类和子类当然是强有力的工具。然而我想说的是,<b>iOS日常开发的问题是对类和继承的过度使用</b>。作为面向对象的编程者(object-oriented programmer,后面统一换为OOP编程者)我们总是会自然的去倾向于引用类型和类去解决问题,但是我个人还是认为应该反过来,倾向于<b>用值类型来代替引用类型</b>。我们还是要去写模块化的,可伸缩的并且可重用的代码,这一点不会变。<b>swift 中有强大的值类型就可以帮我们实现模块化这一目的,且不会对引用类型有过度依赖。</b>我认为不仅面向协议的编程 (protocol oriented programming,后面统一为POP) 可以帮我们实现这点,另外两种类型也可以,且都具有抽象和简化的核心思想,这两种分别是:面向值的编程和 函数式编程

先说清楚,我绝不是这种编程类型 (POP,面向值的编程 和 函数式编程) 的专家。和你一样,从MMM时代(收到内存管理)开始我就是一个OOP编程者。通过自学,从开始就很重视值的抽象和简化思想。我都没有意识到自己是一个倾向于函数式编程的OOP编程者,而且很多时候都是在用面向值的编程和POP的思路。这可能是我为什么在第一天就兴高采烈的加入了swift的浪潮之中的原因。在WWDC的一整周里,swift的核心理念与我认为的该怎样去编程是如此之契合,这个感受一直充斥在我脑海中。通过这篇文章,我希望能帮助你(OOP的编程者)打开思路,去考虑该如何用更加Non-OOP(非OOP)的方式去解决问题。
</small>

<h2>OOP的问题(和我不得不学它的原因)</h2>
我会是第一个跳出来说的:不用OOP的话做出iOS应用很难。Cocoa的核心就是OOP。没有OOP的话你根本写不出来一个iOS应用。有时候我会幻想这不是真的。如果你有不同观点,赶快证明我是错的吧。我真的需要这样,求你了,证明我是错的吧!

不管怎么样,你总会遇到必须用对象、用引用类型解决问题的时候,然后由于Cocoa的规定而被迫使用类(classes)。这种情况下你碰到的问题都是我们大家熟知并热爱的

<small>

  • 传递class的实例这个做法好像总是有种不可思议的能力:你想用一个实例的时候让这个实例的状态和你所期望的不一样。<b>这是由于可变状态导致,你这个对象的另一个享有者在它觉得合理的地方改变此对象的属性。</b>
  • 如果不用多继承的话,从一个很棒的class派生出子类从而获得它的扩展功能,妨碍了你使用另外一些class的更多功能,而且还增加了复杂性。(举个例子来说,把两个UITextField的子类结合起来生成一个拥有这两者功能的UITextField子类,难)
  • 上面一条的另外一个问题是会引出意外行为。如果你遇到了类似上面一条所描述的情况,你就陷入到了一个依赖问题中:你连接了两个superclass各自特性,对其中一个superclass的移除改动可能会给另外一个superclass代理不良影响。这就是class之间紧耦合所带来的问题
  • 单元测试中的mocking。有些class在系统中的耦合过于紧密,想完全测试这些class就需要你创建每一个class的假象表。我都不用告诉你本质上你并没有真正的测试了这个class,你不过是在假装测试它。这里就不提很多Mocking的库是用运行时的小把戏来造一个假的class了。
  • 并发问题。这和上面提到的可变状态是伴随出现的。你从多个线程中同时改变一个引用就会引起这个问题,运行时使对象之间的同步发生异常。
  • 很容易导致出现像上帝类(God classes - 承担着很多subclasses需要的重要高层级代码的所有责任),Blobs(有过多职权的classes),Lava Flow(因为含有太多的非法代码导致任何人都不敢碰的classes)等等这些种反面模式
    </small>


    ggg

<h2>POP 面向协议的编程</h2>
陷入OOP的反面模式特别容易。多半时间我们(包括我)就是太懒而不愿意去点File>New File。结果是在现有class的基础上添加一个函数是如此轻松,我们就不愿意从零开始建一个新的class了。如果你一直这么干,而且一直非常懒的从一个"很重要"的class派生subclass的话,你就把上帝类/死星类给弄出来了。实际上我之前就这么干过:我给一个app里的每个view Controller都加了能呈现一个指向navigationController的navigationBar的error view的功能。唉,我可真蠢。直到要改动那个Error上帝类行为的时候,我不得不把整个app都改一遍。这不是聪明的做法,你真应该看看那些bug。

如果使用了POP,这个Error上帝类很大程度上就能很容易的抽象出来,以后改进它也方便。

这是一个能展示(之前的方式)有多残暴的例子:

class PresentErrorViewController: UIViewController {
    var errorViewIsShowing: Bool = false
    func presentError(message: String = "Error!", withArrow shouldShowArrow: Bool = false, backgroundColor: UIColor = UIColor.red, withSize size: CGSize = CGSize.zero, canDismissByTappingAnyWhere canDismiss: Bool = true) {
        // 写下了复杂的,脆弱的代码
    }
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

随着项目的进行,事情马上变的明了:并不是每一个UIViewController需要这个error逻辑,或者真的需要这个class 所提供的每一个功能。我们团队里任何一个人都可以请用的在这个superclass里改点什么,从而影响整个app。这就让代码变得很脆弱。还是代码呈现出多态。本应该有子类觉得它自己的行为,这里的superclass却给帮着决定了。下面是在swift3.0中的我们如何用POP来更好的构建这段代码:

protocol ErrorPopoverRenderer {
    func presentError(message: String, withArrow shouldShowArrow: Bool, backgroundColor: UIColor, withSize size: CGSize, canDismissByTappingAnywhere canDismiss: Bool)
}
extension UIViewController: ErrorPopoverRenderer {
    //使所有遵从于ErrorPopoverRenderer协议的UIViewController具有一个presentError的默认实现
    func presentError(message: String, withArrow shouldShowArrow: Bool, backgroundColor: UIColor, withSize size: CGSize, canDismissByTappingAnywhere canDismiss: Bool) {
        // 加上呈现error视图的默认实现
    }
}

class KrakenViewController:UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    func methodThatHasAnError() {
        // ...
        // 抛出error,原因是Kraken海妖今天吃人会感到不适。
        presentError(message: "", withArrow: true, backgroundColor: UIColor.red, withSize: CGSize.zero, canDismissByTappingAnywhere: true)
        
    }
}

看,这里发生了很炫酷的事情。我们不仅消除了上帝类存在,还让代码更加的模块化并增强了它的扩展性。通过创建一个 ErrorPopoverRenderer协议,就会让任何则遵循了该协议的class具有呈现出一个ErrorView的能力。还不止这些,我们的KrakenViewController class 不用必须实现presentError这个函数,因为我们扩展了UIViewController,让它提供了一个默认实现。

唉不过等下!这有个问题!我们每次想要呈现一个ErrorView的时候都不想要去实现每一个参数。这就有点儿让人不爽了,因为我们不能再protocol协议函数声明中为参数提供默认值。

我还挺喜欢这些参数的!更槽糕的是在让买卖根据模块化特征的过程中我们引入了复杂度。还是继续吧,用swift3.0中新加的一个小妙招来多少的补偿一下:

protocol ErrorPopoverRenderer {
    func presentError()
}

extension ErrorPopoverRenderer where Self: UIViewController {
    func presentError() {
        // 在这里加默认实现,并提供ErrorView的默认参数。
    }
}
class KrakenViewController: UIViewController, ErrorPopoverRenderer {
    func methodThatHasAnError() {
        //...
        // 抛出error,原因是Kraken海妖今天吃人会感到不适
        presentError()
    }
}

好了,现在看起来已经很不错了。我们不仅消除了这些烦人的参数,还用swift3.0的新特性在protocol的层级上用Self给了presentError一个默认实现。用Self意味着当且晋档协议的遵循者是继承自UIViewController的情况下,这个扩展才会有效。这就让我们能够把ErrorPopoverRenderer真的当做是一个UIViewController,而不需要对后者做扩展!更棒的是,从现在开始,Swift的运行时是以静态调度而非动态调度去调用presentError()方法。大致的意思就是我们在函数调用点给presentError()方法增强了一点性能。

唉,不过还是有个问题。到这里我们POP的旅途暂时告一段落,但对于它的完善依旧不会停止。我们的问题是如果只想对一部分参数使用默认值,对生效的不用默认值该怎么做?在这方面POP的话基本帮不上什么忙,但是我们可以寻求另外一种方法。现在,我们使用面向值的编程(VOP)吧。

<h2>面向值的编程(Value-oriented-programming)</h2>
看到了吧,POP和VOP总是伴随出现。在WWDC视频中,Crusty提出了一下大胆的论断:我们用struct 和 enum 类型就可以做到一切class能做到的事。我很大程度上同意这点,但是没这么极端。依我看,protocol本质上是吧VOP粘合在一起的胶水,这点我和Crusty吃相同太大。实际上既然我们说的了Swift的核心理念以及VOP,我想给你们看看从Andy Matuschak的精彩访谈中关于Swift中的VOP

的话题里面摘出来的一张极好的图:


2.png

能看出来Swift的标准库中,仅有的4个class,和余下的95个struct和enum的实例共同构建了Swift功能的核心。

Andy如此阐述道:用Swift编程的时候我们要考虑用一层很薄的对象层,和一个很厚的值类型层。Class是有它们的地方,但是我想尽最大程度的去认为它们的位置只应该处于对象层中的一个很高的级别上,在这里通过操纵值类型层中的逻辑来管理各种行为。

"把逻辑和行为分开"——Andy Matuschak

和你所了解的一样,值类型被赋给一个变量或者常量,抑或是传给函数做参数时是它的值被拷贝的。这就让值类型在任何时候只有一个享有者,从而降低复杂度。和引用类型相反,在赋值过程中引用类型会有很多享有者,其中一部分你甚至都没意识到。在任何时间点使用引用的话会带来一些副作用:引用的享有者会捣蛋,在背后偷偷改变这个引用。Class = 高复杂度,值 = 低复杂度。

通过利用值类型的简约特性,咱们实现一下之前提过的默认参数的设计吧。我们用的是 Brian Gesiak的value options paradigm方法:

struct ErrorOptions {
    let message: String
    let showArrow: Bool
    let size: CGSize
    let candismissByTap: Bool
    let backgroundColor: UIColor
    init(message: String = "Error", shouldShowArrow: Bool = true, backgroundColor: UIColor = UIColor.red, size: CGSize = CGSize.zero, canDismissByTappingAnywhere canDismiss: Bool = true) {
        self.message = message
        self.backgroundColor = backgroundColor
        self.size = size
        self.candismissByTap = canDismiss
        self.showArrow = shouldShowArrow
    }
    
}

使用上面的选项型struct(是值类型!)就使我们的POP带上了一些VOP的色彩,如下:


protocol ErrorPopoverRenderer {
    func presentError(_ errorOptions: ErrorOptions)
}

extension ErrorPopoverRenderer where Self: UIViewController {
    func presentError(_ errorOptions: ErrorOptions) {
        // 在这里加默认实现,并提供ErrorView的默认参数。
    }
}
class KrakenViewController: UIViewController, ErrorPopoverRenderer {
    func methodThatHasAnError() {
        //...
        // 抛出error,原因是Kraken海妖今天吃人会感到不适
        presentError(ErrorOptions(message: "Oh noes! I didn't get to eat the Human!", size: CGSize(width: 1000.0, height: 20)))
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容