首先更正昨天文章《App设计模式纵横谈(1)》的一个错误,
这里单一职责原则和“我们应该多用类的引用,而不是类的继承”,并没有因果关系
感谢网友指出。这句话我昨晚想了一下,应该放在“开闭”原则下面,是非常合适的。我不该放在这个位置。已经在文章中更正。
其次,写了RN和设计模式2篇文章了,都是概论或序言,有人会不喜欢。其实呢,我想说一下我的思路。
别人写设计模式都是23个模式逐个讲一遍,贴代码,写原理,画UML图,介绍使用场合、有缺点。我如果也这样写一遍,就雷同了。所以我准备从设计原则这个维度来讲,路上遇到哪个模式,就顺带讲一下。
所以设计模式这个专题,可能是一系列散文,偶尔夹杂一些代码,能让你读起来轻松些,莞尔一笑,无论是在上班的路上或者下班的路上。
那我们今天就从“我们应该多用类的引用,而不是类的继承”这个点讲起来。
(一)
现实中还是有很多开发者并没有理解这个思想。比如说,封装网络底层框架,现在都用OKHttp,以下是发起一个简单的网络请求:
但是项目中并不直接使用OKHttp的OkHttpClient类,而是在外面简单的包装一下。就是这里,会有两种做法:
做法1:写一个继承自OkHttpClient的子类,比如MyOkHttpClient,然后这这个子类中扩展一些更高级的特性。
这种做法短时间没毛病,如果你只准备在这家公司做一两年。
但是,这种思路经不住App技术的快速发展,如果几年后又出了一个新的网络框架,比如就叫BaobaoHttp吧,它提供了BaobaoHttpClient这个对象用于发起网络请求,而OkHttp废弃了不再使用了,这时我们就要做架构升级,你会发现,居然要改那么多东西,因为我这里用的是继承,OkHttpClient废弃了,MyOkHttpClient也就不能再使用了。为此我要修改一堆使用了MyOkHttpClient类的地方。这样设计有问题。
做法2:我写一个RemoteService类,它内部保存了一个OkHttpClient对象。
我通过在RemoteService中自定义一些网络请求方法,然后在这些方法中,实际是操作OkHttpClient。这种设计就不会因为OkHttpClient框架被废弃,而需要改动太多的地方,只要在内部再保存一个BaobaoHttpClient的对象就好了,只要把RemoteService中所有用到OkHttpClient的地方,都改为BaobaoHttpClient对象。
而对于上层来说,还是调用RemoteService的各种方法,完全感知不到RemoteService到底用的是哪个网络框架。
这就是对“我们应该多用类的引用,而不是类的继承”这个思想的阐述。纵观GOF的23个设计模式,在他们的UML图中,但凡是有实心箭头的(实心箭头表示引用),都蕴含着这个思想。因为太多了,我找了其中有代表性的3个。
1)Builder生成器模式,请注意Director指向Builder的那根线。
2)备忘录模式,请注意Originator和Memento指向State的那两根线。
3)Command命令模式,请注意Client指向Invoker和Receiver的那两根线,以及Invoker指向Command接口的那根线。
(二)
看了这几个图,你会发现,为啥连接线都不太一样呢。这就涉及到类与类之间的四种关系了(也有说6大关系的,这里就不展开了)。
1)依赖(Dependency),用的是虚线,空箭头。
ClassA的do方法,参数中包含ClassB对象。就是二者在方法参数上有说不清道不明的关系 :)这种关系很弱很弱。
2)关联,用的是实线,空箭头
一个类保持对另一个类的引用,通过属性/变量的方式,建立连接。
箭头两端的1和0..*是什么鬼?在连接线的每一端都可以有一个基数,一共有4个值:
图中的例子就是,人和宠物是一对多的关联关系,人可以有或没有宠物(为0)。
还有一种场景,那就是自己关联自己,比如一个人可以有多个朋友,这些朋友肯定也是人:
如果你看过职责链模式,没错就是这个画法。
3)引用/组合(Composition),实线,实心菱形。
引用,也称为组合。
这是一种ContainsA的关系。
几个类是同生共死的关系,就比如说鸟和它的翅膀、尾巴。
它是关联关系的特例。
4)聚合(Aggregation),实线,空心菱形。
这是一种Has A的关系。
左边是集合(整体),右边是单个元素(部分)。
整体和部分,有各自的生命周期。敢死队员挂了,但是敢死队还在,他们没有同生共死的概念。
这种绘制方式,多用于集合类。比如说组合模式和命令模式。
(三)
还是回到“我们应该多用类的引用,而不是类的继承”这个点,谁让咱昨天把这句话放错地方了呢。前面讲了那么多,你们应该已经知道这句话的意义了。
为什么我说它应该放在“开闭原则”里面呢?因为开闭原则,也就是Open-Close讲的是对扩展开发,对修改封闭。
怎么理解呢?
我们日常写得最多的就是Activity和ViewController,你有没有看过那个类的代码最多,根据我的经验,一般是订单填写页,动辄五六千行代码,一个类代码太多,就很难维护,经常为了做一个新功能改两行代码,就把之前好的功能给改坏了。这里面肯定有不符合开闭原则的地方——“对修改封闭”,不要试图去修改原先的类。
为此我们要把这个类的五六千行代码拆分成若干个类,说的夸张些,宁可有50个100行代码的类,也比之前一个类5000行代码要好。拆分为之后,再有什么代码改动,也只会涉及到其中的2-3个类,这称之为“对扩展开放”,采取增加一个类的方式,来增加新功能。
(四)
说了半天理论,我们看一个最简单的例子,switch…case语句。这个例子我经常拿来举例。
如果一个页面,有三种交通工具可以选择,那么就要在这个页面类的每个方法中都要用switch…case来判断一遍,贴几行代码:
都回去看你们的项目,肯定有类似的代码,使用if...elseif...else...这样的语句也算。
这样做乍一看也还OK,但是一旦增加一种新的交通工具,那么你就需要在setup、deive、stop方法中都增加一种case判断,用来处理这种新的交通工具。这就违背了我们前面讲的开闭原则了。因为我们对修改开放了。
那么能否按照开闭原则,通过增加一个类(交通工具)的方式,从而不用修改这个页面所在的类吗?
答案当然是可以的,否则我就写不下去了。
我们引入一个基类,所有交通工具的基类Vehicle,在这个基类中定义setup、deive、stop这三个方法:
然后让所有的交通工具,都继承自这个基类,并实现这三个方法,以Car为例:
那么在页面类中,我就可以不要再声明3种交通工具的类对象了,只要用一个Vehicle对象的声明即可,具体是哪个,在运行期间再指定,这就是大家都听说过的“惰性加载”,看以下代码片段:
虽然在上面的代码中,我们还是要用到一次switch case语句,来判断应该把Vehicle实例化为哪一种交通工具对象,但是也就这一次,接下来的几个方法就非常简单了:
是不是觉得代码清爽了很多,switchcase语句只出现了一次,后面就可以全都面相Vehicle对象进行编程了。
新的UML设计图是下面这样:
在此基础上,如果新增一种交通工具,比如说地铁,那么只要新增一个Subway类,继承自Vehicle基类,实现Vehicle的三个方法。然后在switch case语句中增加一种case情景,而其他地方不需要再做任何改动:
本文相关代码请参见:http://files.cnblogs.com/files/Jax/dp.zip
这个交通工具的例子,再往前走一步,就是简单工厂模式了。我下一篇文章准备把各种工厂一网打尽,敬请期待。