如果你还在用子类(Subclassing),那就不对了
本篇文章翻译自:IF YOU'RE SUBCLASSING, YOU'RE DOING IT WRONG.
原作者:Hector Matos
原发表日期:2015-07-13
Swift的核心
我们可以通过等式的传递性来理解swift:
- Swift的核心是面向协议的编程。
- 面向协议的编程的核心是抽象(abstraction)和简化(simplicity)。
- 所以swift的核心就是抽象和简化。
你可能对我的标题感到诧异。我并不是说子类没有价值,尤其在使用单一继承(single inheritance)的情况下,类和子类当然是强有力的工具。然而我想说的是,iOS日常开发的问题是对类和继承的过度使用。作为面向对象的编程者(object-oriented programmer,后面统一替换为OOP编程者;object-oriented programming后面统一简写为OOP)我们总是会自然的倾向于使用引用类型和类去解决问题,但是我个人还是认为应该反过来,倾向于用值类型代替引用类型。我们还是要去写模块化的,可伸缩的并且可重用的代码,这一点不会变。swift中强大的值类型就可以帮我们实现此目的,且不需要对引用类型有过强的依赖。我认为不仅面向协议的编程(protocol oriented programming,后统一替换为POP)可以帮我们实现这点,另外2种编程类型也可以,且都具有抽象和简化的核心思想,这两种分别是:面向值的编程(value-oriented programming,后面统一替换成VOP)和函数式编程(functional programming)。
先说清楚,我绝不是这些种编程类型(POP,VOP和函数式编程)的专家。和你一样,从MMM时代(manual memory management - 手动内存管理)开始我就是一个OOP编程者。通过自学,从开始我就很重视值抽象(value abstraction)和简化的思想。我都没有意识到自己是一个倾向于函数式编程(functional programming)的OOP编程者,而且很多时候用的都是VOP和POP的思路。这可能是我为什么在第一天就兴高采烈的加入了swift的浪潮之中的原因。在WWDC的一整周里,swift的核心理念与我认为的该怎样去编程是如此之契合,这个感受一直充斥在我脑海中。通过这篇文章,我希望能帮助你(OOP的编程者)打开思路,去考虑该如何用更加Non-OOP(非OOP)的方式去解决问题。
OOP的问题(和我不得不去学它的原因)
我会是第一个跳出来说的:不用OOP的话做出iOS应用很难。Cocoa的核心就是OOP。没有OOP的话你根本写不出来一个iOS应用。有时候我会幻想这不是真的。如果你有不同观点,赶快证明我是错的吧。我真的需要这样,求你了,证明我是错的吧!
不管怎么样,你总会遇到必须用对象、用引用类型解决问题的时候,然后由于Cocoa的规定而被迫使用类(classes)。这种情况下你碰到的问题都是我们大家熟知并热爱的:
- 传递class的实例这个做法好像总是有种不可思议的能力:你想用一个实例的时候,让这个实例的状态(state)和你所期望的不一样。(这是由于可变状态(mutable state)导致,你这个对象的另一个享有者在它觉得合理的时候能够改变此对象的属性。)
- 如果不用多继承的话,从一个很棒的class派生出子类从而获得它的扩展功能妨碍了你使用另外一些很棒的class的更多更能,而且还增加了复杂性。(举例来说,试着去把2个
UITextField
的子类结合起来,生成一个拥有这2者特性的超级UITextField
吧。) - 上面一条的另外一个问题是会引出意外行为(unexpected behavior)。如果你遇见了类似上面一条所描述的情况,你就陷入到了一个依赖问题中:你连接了2个superclass各自的特性,对于其中一个superclass的一处改动可能会给另外一个superclass带来不良影响。这就是被周知的class之间紧耦合(tight coupling)所带来的问题。
- 单元测试中的mocking。有些classes在系统中的环境状态下耦合过于紧密,想完全测试这些classes就需要你创建每个class的假表象。我都不用告诉你本质上你并没有真正的测试了这个class,你不过是在假装测试它。这里就不提很多Mocking的库是用运行时的小把戏来造一个假的class了。
- 并发(Concurrency)问题。这和上面提到的可变状态是伴随出现的。你从多个线程中同时改变一个引用就会引起这个问题,在运行时使对象之间的同步发生异常,这点也真的不用和你说了。
-
很容易导致出现像上帝类(God classes - 承担着很多subclasses需要的重要高层级代码的所有责任),Blobs(有过多职权的classes),Lava Flow(因为含有太多的非法代码导致任何人都不敢碰的classes)等等这些种反面模式(anti patterns)。
能看出来Swift的标准库中,仅有的4个class,和余下的95个struct和enum的实例共同构建了Swift功能的核心。
Andy如此阐述道:用Swift编程的时候我们要去考虑用一层很薄的对象层,和一层很厚的值类型层。Class是有它们的地方,但是我想尽最大程度的去认为它们的位置只应该处于对象层中的一个很高的级别上,在这里通过操纵值类型层中的逻辑来管理各种行为。"把逻辑和行为分开"
-Andy Matuschak和你所了解的一样,值类型被赋给一个变量或者常量,抑或是传给函数做参数时是它的值被拷贝的。这就让值类型在任何时候只有一个享有者,从而降低复杂度。和引用类型相反,在赋值过程中引用类型会有很多享有者,其中一部分你甚至都没意识到。在任何时间点使用引用的话会带来一些副作用:引用的享有者会捣蛋,在背后偷偷改变这个引用。Class = 高复杂度,值 = 低复杂度。
通过利用值类型的简约特性,咱们实现一下之前提过的默认参数的设计吧。我们用的是 Brian Gesiak的value options paradigm方法:
struct Color { let red: Double let green: Double let blue: Double init(red: Double = 0.0, green: Double = 0.0, blue: Double = 0.0) { self.red = red self.green = green self.blue = blue } } struct ErrorOptions { let message: String let showArrow: Bool let backgroundColor: UIColor let size: CGSize let canDismissByTap: Bool init(message: String = "Error!", shouldShowArrow: Bool = true, backgroundColor: Color = Color(), size: CGSize = CGSizeZero, canDismissByTappingAnywhere canDismiss: Bool = true) { self.message = message self.showArrow = shouldShowArrow self.backgroundColor = backgroundColor self.size = size self.canDismissByTap = canDismiss } }
使用上面的选项型
struct
(是值类型!)就使我们的POP带上了一些VOP的色彩,如下:protocol ErrorPopoverRenderer { func presentError(errorOptions: ErrorOptions) } extension ErrorPopoverRenderer where Self: UIViewController { func presentError(errorOptions = ErrorOptions()) { //在这里加默认实现,并提供ErrorView的默认参数。 } } class KrakenViewController: UIViewController, ErrorPopoverRenderer { func failedToEatHuman() { //… //抛出error,原因是Kraken海妖今天吃人会感到不适。 presentError(ErrorOptions(message: "Oh noes! I didn't get to eat the Human!", size: CGSize(width: 1000.0, height: 200.0))) //Woohoo! 没有参数了!我们现在有默认实现了! } }``` 如你所见,对于用```view controller```做error处理,我们给与它了一种完全抽象的,可伸缩的和模块化的方式,还不用强迫所有的```view controller```去继承一个上帝类。当你有一个具有不同功能的上帝类的时候,上面的例子尤其能帮到你。除此之外,用这种方式去实现类似上面error功能的其他功能时,你把实现该功能的代码放哪儿都行,不必做太多的重构或者改变代码框架。 ![](http://static1.squarespace.com/static/5592eb03e4b051859f0b377f/t/55a34ee8e4b0cc071f51da40/1436765929441/?format=750w) ##函数式编程 咱们来解决这个。我也刚开始接触函数式编程,不过我知道一点:这种范式(paradigm)要求一种鼓励编程者去避免可变数据(mutable data)和改变状态(changing state)的编程方式。和数学函数类似,函数式编程是由一些输出结果仅取决于输入参数的函数组成,而且函数的输出结果不会被本体之外的相依性(dependency)所影响。这就是众所周知的"data in, data out",意思是每次传进来一个值,这个值传出去的时候和传进来时候总要是一样的。**想想单元测试就明白了**! 如果我们用函数式的思想去写代码,就可以把VOP与函数式编程结合,利用其中的诸多优点,这些优点包括但不仅限于: - 完全线程安全的代码(值类型变量在并发代码中被分配时是被拷贝的,意思是另一个线程更改不了与它平行线程中的变量)。 - 更详尽的单元测试 - 不再需要在单元测试中用mock(用了值类型的变量就不用再重建一个必须使用mock对象的环境,只为了去测试仅仅少部分的功能。本质上通过初始化一个从任意依赖关系中抽象出来的特性,你可以重建任何你想要的东西。) - 代码更简洁(说实话,能和[瓷器](https://www.youtube.com/watch?v=estNbh2TF3E)一样精致)。 - 让你身边的小伙伴惊呆 - 很炫酷 - 让Kraken疯狂的崇拜你 ##什么时候用子类 什么时候应该用子类呢?答案是当你没选择的时候。比如: - 当系统要求的时候。许多Cocoa的API要求你使用class,你不应该非要用值类型来跟系统对着干。```UIViewController```是要派生子类的,要不然你的app就啥都没有了。**别跟系统对着干!** - 当你需要有东西来帮你管理在其他class实例之间的值类型变量,而且还需要与这些值类型变量通信的时候。对于这种情况Andy Matuschak给了一个很好的例子:用一个class把一个值类型的绘图系统计算好的值取过来,传递给一个Cocoa的class来把这个绘图系统绘制到屏幕上。 - 当你需要或者想在许多享有者之间做隐式共享的时候。此种情况的例子是Core Data。数据持久化变幻无常,用Core Data的时候,使用子类给诸多需要同步的享有者做同步就很有效。但是要小心并发问题!这是你处理此类问题的时候必须要做的取舍。 - 当你不知道对于引用类型来说它的拷贝意味着什么的时候。你会拷贝一个单例么(singleton)?不会。你会拷贝一个```UIViewController```么?不会。一个```window```?绝对不会。(你其实可以,这是你的特权。) - 当一个实例的声明周期与外部效应(external effect)绑定的时候,或者就只是需要一个稳定个体(stable identity)的时候。单例就是特别典型的例子。 ##结论 作为OOP的编程者我们已经习惯了用class来解决问题。长期以来我们开发了很多模式来弥补引用类型所带来的弊端。我的观点是在编程中换一种思路可以有效的减轻对这类折衷方案的使用。如果我们真的重视可伸缩性和可重用性,就得接受模块化的编程才是正道。使用值类型并结合Swift 2.0中新增并改进了的protocol特性就会轻松的达到这个目的。虽然之前OOP的思维方式会使我们比较难用VOP和POP的方式来思考,但是在swift中写的多了,VOP和POP的模式就会开始成为我们的第二天性。我们的大脑可能得需要我们多写一些代码才能适应这种思维方式,但我相信iOS社区作为一个整体能接纳这些做法,从而极大的降低我们日常解决问题的难度。Swift的核心是一个**极为**强大的值类型系统,坦白说,我们应该一开始就用VOP的思想磨练自己来发扬这个值系统的优势。但愿这篇文章能多多少少的帮助到你,让你每天写出来更加详尽的,天生安全的代码。 ![祝码农们编程愉快!](http:https://img.haomeiwen.com/i131207/022fe67f815ca17f.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240)