iOS代码设计中的开放与封闭
Chandra20S
8年前
<p>我们至今所写的 iOS 代码都是遵循 OOP 这种编程范式,以对象来临摹和表达我们对于世界的理解。在设计类的时候,恪守 SOLID 五个原则会让我们的代码更易拓展和维护。SOLID 中的 O 代表的是 <strong>Open/closed principle</strong> ,这篇文章所要探讨的不仅仅是类设计中的 Open 和 Closed,而是要站在更广阔的视角来看待代码中的开放与封闭。</p> <h3>前言</h3> <p>我们作为代码工作者,不能仅仅满足于写出能运行的代码,还是注意时刻提高自身的姿势水平。具体来说,就是加强对于「内功心法」的学习,逐步提升写代码的抽象和设计能力。</p> <p>程序员是理工教的一大分支,我们向来以严密的逻辑推导能力为立身之本,我们很容易发现文科生思维中存在的逻辑不连贯,不缜密,不严格,我们擅长以 if, else, for, switch 等精巧的关键字来阐述逻辑和流程。用代码来表达的流程看上去确实很酷,很科学,很真理,可在数学家眼里,我们大部分程序员所写的代码其实「漏洞百出」,和「严密」二字几乎不怎么沾边,看起来并不比文科生高明多少。问题出在哪呢?姿势水平还不够。</p> <h3>Open vs Closed</h3> <p>我们先以 Open/closed principle 为切入点,对于代码的开放和封闭来建立初步的印象。Wikipedia 定义如下:</p> <p>In object-oriented programming , the <strong>open/closed principle</strong> states “ <em>software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification</em> .</p> <p>这一原则要求我们设计的功能单元,对于功能拓展是开放的,而对于代码修改则是封闭的。不知道大家对于这种抽象的描述作何感想,Peak君初次看到的时候,脑中只感觉一团云雾缭绕,怎么能让马儿跑,又不用吃草?</p> <p>这段玄之又玄的描述,体现到代码中后,不过就是一些日常所用的语言技巧了,我们可以从多个角度去理解和实现代码的开放性和封闭性。</p> <p>继承,简单的继承关系就可以体现 open/closed principle,如果我们设计好一个父类,这个父类在设计之初就已经有了清晰完备的功能定义,并向天起誓以后绝不修改这个父类一行代码,那么我们可以说这个父类已经 closed 了。想要拓展功能怎么办?新建一个子类继承自这个父类,在子类中添加我们所需要的新功能,这样就做到了 open。一言蔽之,父类对于代码修改是封闭的,而对于子类的功能拓展是 open 的。实际工程中,多少人能忍得住不修改父类呢?</p> <p>多态,多态配合接口使用也能体现 open/closed principle,我们在设计功能单元的时候,只定义接口,而不规定具体实现的细节。在类或者模块交付的时候,我们继续向天起誓以后绝不修改接口中的定义,那么接口就是 closed 了。但是后期我们可能需要修改具体实现的细节,需要拓展功能,于是我们替换一个实现了该接口的另一个类,这个新的类实现对于代码修改是 open 的。简而言之,接口对于代码修改是封闭的,实现对于代码修改是开放的。这也是为什么,我们在写 iOS 代码的时候,需要大量运用 protocol。</p> <p>这么看来,开放和封闭的定义还是很清晰的,二者针对的对象不同就可以合理共存。不过我们为什么既要封闭又要开放呢?因为封闭的事物是静态的,稳定的,安全的,不写一行代码就不会有 bug 不是吗?可是我们所做的每一个工程都是处于变化的状态,每一个新 feature 都是为了迎合不断变化的市场需求,所以 open 是不可避免的,怎么办呢?让 open 与 closed 并存,让稳定的部分不变,在 closed 的代码基础之上去做拓展,去 open 新的代码。</p> <h3>Algebraic data types</h3> <p>聊完了我们熟悉的继承和多态,下面我们进入一个稍微陌生一些的领地:Algebraic data types。</p> <p>Algebraic data types 是纯函数式编程语言 Haskell 中的一种类型定义,这是一个看上去简单,实际上令初学者极其费解的技术概念。之所以费解,是由于它主要应用在数据模型的定义,和我们平常写业务所用的 int, float 这种 data type 完全不是一回事。</p> <p>Algebraic data types 可以简单的理解为一些 data type 的集合,这里的 data type 就是我们传统意义上的数据类型,比如 bool, int, double 等等,在这个 data type 的集合之上,Algebraic data types 提供一些特定的代数操作,可以对 data type 集合里的每个 data type 执行逻辑。代数操作通常为两种:sum 和 product。很抽象是不是?到底有什么用?我们对应到 iOS 中的代码来理解下。</p> <p>比如我们日常所用的 BOOL 类型:</p> <pre> <code class="language-objectivec">BOOL isValid = true; isValid = false;</code></pre> <p>isValid 的值要么是 true 要么是 false,是二选一的关系,所以 isValid 的值有两种可能性,即 true 和 false 相加,所以 BOOL 类型可以理解成一种 sum type。</p> <p>再看 CGPoint :</p> <pre> <code class="language-objectivec">struct CGPoint { CGFloat x; CGFloat y; };</code></pre> <p>x 和 y 同时存在于 CGPoint 这个类型当中,不是二选一,而是一种类似于组合同时存在的关系,我们把 CGPoint 这种由两个子 type 所共同构成的 data type 称之为 product type。</p> <p>你可能发现了,所谓的 sum type 和 product type 就是对 data type 集合中的元素进行 and 或者 or 操作,从而拼装出各种可能的组合。OOP 下的 data type(比如我们自定义的 class)强调的是对于 property 和 function 的封装,而 Algebraic data types 完全换了一个视角,看重的是 data type 的组合方式。当我们以递归的方式使用 Algebraic data types 来描述各种 data 的时候,就开启了一扇新世界的大门。</p> <p>sum type 和 product type 都是 Algebraic data types。按照这种规则定义的 data type 到底有什么用处?好处有很多,其中之一和这篇文章的主题相关。Algebraic data types 有个重要的特性:Algebraic data types 对于自身 data type 集合中的每个 type 的处理是以穷举的方式,而且 data type 集合中的一旦定义好之后是不允许修改的,closed!这一点和我们在 OOP 下自定义的 Model Class 非常不同,Class 是允许被继承来拓展功能的,而 Algebraic data types 一旦定义好就已经 closed 了。</p> <p>比如 isValid 如果定义包含 true 和 false 之后,是不允许添加 half-true 的,同时所有对于 isValid 的操作要穷举 true 和 false 两种可能性。</p> <p>Algebraic data types 的 closed 和 exhaustive 特性可以让代码更加稳定,当然这种特性需要语言层面的支持,Objective C 并没有相关的特性,但我们可以在代码设计中借鉴其思想。</p> <p>我们在平时写业务的时候,经常需要设计各种各样的 model 类。非死book 在 2016 年开源了一个专门用来管理和生成 model 的 framework,叫做 Remodel 。这个库功能强大而且全面,其中之一就是生成符合 Algebraic data types 特性的 model。以如下代码为例,描述的是一个具有多种类型的消息 model:</p> <pre> <code class="language-objectivec">@interface MessageContent : NSObject <NSCopying, NSCoding> + (instancetype)imageWithPhoto:(Photo *)photo; + (instancetype)stickerWithStickerId:(NSInteger)stickerId; + (instancetype)textWithBody:(NSString *)body; - (void)matchImage:(MessageContentImageMatchHandler)imageMatchHandler sticker:(MessageContentStickerMatchHandler)stickerMatchHandler text:(MessageContentTextMatchHandler)textMatchHandler; @end</code></pre> <p>MessageContent 有三种可能的类型,image, sticker, text。MessageContent 提供的 match 方法以穷举的方式来处理所有可能的场景,对于 MessageContent 的使用者来说,一定不会漏处理任何一种可能性,强制 model 的使用者考虑所有的场景。</p> <p>这种做法的好处是代码一旦生成就极其稳定可靠,不允许修改,closed。缺点也很明显,一旦业务要求我们增加一种新的 type,比如 MessageContent 为 voice 的语音消息,会难以下手,因为一旦修改就必须改变 match 方法签名,以穷举的方式新增一种 type 处理,代码的改动牵涉面必然很广。</p> <p>所以你看,到底是设计成 closed 还是 open 的,其实是一次根据业务场景的取舍,在变与不变之间做权衡。这里介绍 Algebraic data types 目的在于说明,我们在做代码设计的时候,closed 和 exhaustive 的设计方式会让我们的代码更加可靠和稳定。</p> <h3>Optional in Swift</h3> <p>刚开始学习 Swift 的时候,不知道大家有没有好奇过为什么要引入 optional 这样一个新类型,optional 使用的场景也非常之多,有很多的文档去介绍在不同的语法下 optional 如何使用,可为什么要 optional 呢?和我们用 Objective C 时判断是否为 nil 有什么区别呢?</p> <p>我们先看下面一段函数:</p> <pre> <code class="language-objectivec">- (User*)getLuckyUser { //perform some calculation... return _user; }</code></pre> <p>这段很常见的代码没有考虑一种场景,就是 _user 为 nil 的情况。你可能会说函数返回 nil ,函数的调用方自己去判断就可以了。当然如果返回 nil,在 Objective C 的 runtime 里,给 nil 对象发送消息也是安全的,这种安全只是表示不会 crash,但有可能原本应该执行的逻辑就没有继续下去了,从这一角度去看,nil 对象是对业务不安全的。而且我们把这种 nil 的 case 所造成的影响延迟到了 run time 。</p> <p>更合理的做法是在编译时就考虑 nil 这种 case。optional 正是为此而生,如果我们定义返回值为 optional,那么 optional 的使用方就一定要考虑值不存在的场景,如果漏处理了为 nil 的场景,就会编译器报错,这样不光不会 crash,而且对业务逻辑来说也是安全的。</p> <p>感觉灵敏的同学可能发现了,optional 类型和上面提到的 Algebraic data types 中的 sum type 非常相像,它表达的也是一种 or 的关系,即值要么存在,要么为 nil。当我们使用 Algebraic data types 来描述 data 的时候,语言本身会强制我们做 exhaustive checking,去考虑 data 的所有可能性。这是另一个 Swift 比 Objective C 更安全的有力证据,Swift 吸收了函数式编程语言中的很多优秀特性。</p> <p>总结就是,当我们使用 optional 来写业务的时候,Swift 会强制我们去考虑 data 的各种可能性,这样写出来的函数,其逻辑就是完整的,全面的。</p> <h3>总结</h3> <p>还有不少能体现 open 和 closed 设计思想的例子,比如 java 中的 final 关键字,又比如设计模式中的 Visitor Pattern,大家也可以联想下类似的例子。我个人比较喜欢写这类随意遐想的文章,畅想不同技术概念之间在设计思想上的关联,加以总结和巩固。</p> <p> </p> <p>来自:http://mrpeak.cn/blog/ios-close-open/</p> <p> </p>