深入浅出系列第一篇(设计模式之单一职责原则)—— 从纯小白到Java开发的坎坷经历
各位看官大大们,晚上好。好久不见,我想死你们了...
先说说写这个系列文章的背景:
工作了这么久了,每天都忙着写业务,好久没有好好静下心来好好总结总结了。正好这段时间公司组织设计模式的分享分,所以我才有机会在这里和大家唠唠嗑。
也许因为自己是小白自学的吧,所以磕磕绊绊走了好多弯路。所以我深刻的理解到在自学的时候有一个前辈在前面引路是多么重要。可以让你少走很多弯路。
拥有更高的学习效率。特别是在一些问题上,苦思冥想很久都没有结果,白白浪费了很多时间,也许有人点拨一下就能茅塞顿开。
有这么一句话说的:如果你想真正掌握某个知识,那就尝试把它教给别人吧。所以我苦思冥想了很久终于决定写一个系列的文章,是为了帮助别人也是为了提升自己。
好了话不多说,下面咋们上干货。
说道设计模式呢,我相信很多人都学习过。但是设计模式到底是什么呢?我相信每个人心中都有自己的答案,就像一千个人心中有一千个哈姆雷特。
那么他到底是什么呢?从字面意思来说,它就是代码设计时解决某些问题的套路。(哎...自古真情留不住唯有套路得人心啊。)
好了,看到这看官们就要问了,你这说的啥玩意,干货呢?尽在这扯玄学,这玩意到底应该怎么学习?
各位看官莫慌,下面就带大家进入设计模式的海洋。设计模式多不胜数,常见的设计模式就有23种(工作中我也只用到了代理模式,策略模式,单例模式,工厂模式和建造者模式)。
在学习他们之前,我们先要了解这些模式的设计思想,所谓知其然还要知其所以然。下面要讲到的就是设计模式的六大原则之单一职责原则。
单一职责原则(Single Responsibility Principle)简称SRP:
定义:There should never be more than one reason for a class to change(原文解释)
中文解释:应该有且仅有一个原因引起类或者方法的变更
单一职责原则的好处:
- 类的复杂性降低,实现什么职责都有清晰明确的定义。
- 复杂性降低也降低了出现问题的概率,并且提高了可读性和可维护性。
- 变更引起的风险降低,变更是不可避免的,如果接口的单一职责做得好,一个接口的修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
看到这的看官是不是准备开骂了。这他妈什么玩意,讲了半天,还是照本宣科。搞个锤子!
各位看官不要急,请往下看。
先请各位看一个类图:
各位看官大大有没有发现什么异常?
现在我来揭晓答案:
这是举例的是一个我工作中遇到的反面例子,这个抽象类和接口很明显的违反了单一职责原则。它包含了数据采集,解析,转换,清洗,映射,计算,保存等多种职责。
期初是为了方便开发,大家便绞尽脑汁封装了一个公共处理类供大家使用。开始项目初期数据来源少,并没有发生什么问题,大家也很满意。
但是(大家注意转折来了)随着项目的推进,数据来源、数据类型越来越多,随之而来的数据映射方式也越来越多。
为了适应各种映射方式大家开始在公共类中加入一些可以覆写的空方法,用子类继承公共类覆写某些方法来实现改变(这里使用的是模板设计模式)。
这也导致这个公共基类被频繁修改经常出现各种莫名其妙的bug,很多之前好的功能也会突然出现问题。而且后来方法越加越多,原先的方法也越来越长逻辑越来越难看懂,导致后面甚至都无法维护,
大家焦头烂额,最后只能推到从来重新划分职责细化方法。最后我们花费了大量的人力物力将之前的抽象类改造成了如下所示五个类:
AbstractDataCapture类:职责是数据采集 由它专门负责数据采集,该类包含主要抽象方法是数据采集方法(由具体子类自己实现)和一些辅助方法。
AbstractDataParser类:职责是数据解析,它专门负责数据解析,该类包含主要抽象方法是数据解析方法(由具体子类自己实现)和一些辅助方法。
AbstractDataCleansing类:职责是数据清洗,它专门负责数据转换成映射model类和清洗数据,该类包含主要抽象方法是数据转换方法和数据清洗方法(由具体子类自己实现)以及一些辅助方法。
AbstractDataHandler类:职责是数据处理,它专门负责数据处理,该类包含主要抽象方法是数据关系映射和数据的计算处理(由具体子类自己实现)以及一些辅助方法。
AbstractDataSave类:职责是数据持久化,该类包含主要方法是数据持久化方法(非抽象方法有公共实现)和一些辅助方法。
这是我实际经历的事情,这是一次血的教训。
以下例子来自于《设计模式之禅》一书。推荐一读
不知道各位看官有没有看懂,没看懂的话没关系。我摘录了一本书上的例子。
如果要你设计一个电话类,你会怎么设计?是不是像下面这样
public interface Phone {
//拨通电话
public void dial(String phoneNumber);
//通话
public void chat(Object o);
//通话完毕 挂断电话
public void hangup();
}
大家看看这个接口有没有什么问题?我相信大家都会说,这个没什么问题啊。我平时也是这么写的,某某框架的源码中也很多这样的接口啊。
大家仔细看可以发现,这个接口不止一个职责。它包含了两个职责:
- 协议管理(dial方法和hangup方法)负责拨号接通和挂机
- 数据传输(chat方法)负责把对方传递过来的信号还原成声音信号
我们可以这样考虑问题,协议连接的变化会引起这个接口或者实现类的变化吗?会的!那数据传输的变化会引起这个接口或实现类的变化吗?
答案也是会的。那就简单了,这里有两个原因都会引起接口或者实现类的变化。而且这两个职责的变化不互相影响,我们可以考虑将其拆分成两个接口。
但是这个类图虽然满足单一职责原则,但是我相信看官们在设计的时候不会才采用这样一种方式。一个手机要把ConnectionManager类和 DataTransfer类组合在一块才能使用。
组合是一种强耦合关系,你和我都有共同的生命期,这样的强耦合关系还不如使用类直接实现接口的方式,这样还增加了类的复杂性,多了两个类。于是我们决定在修改一下。
这样设计就比较完美的。一个类实现了两个接口,把两个职责融合在一个类中。你会觉得这个Phone有两个原因引起变化啊,是的,但是别忘记了我们是面向接口编程,
我们对外公布的是接口而不是实现类。而且如果要真的实现类的单一职责原则,就必须使用之前的组合模式。这样会引起类见耦合过重,类的数量增加等为题,人为的增加了设计的复杂性。
总结:
大家看了之后是不是像反思一下了,我以前的设计是不是有问题了?不,并不是这样的,不要怀疑自己。
原则本身是非常优秀的,但是现实有现实的难处,因为职责和变化因数难以被具体量化。
而且在日常开发中,我们还要考虑工期,成本,人员技术水平等等多种情况。原则是死的人事活的。
我们应该审视夺度,在适当的时候用适当的设计。我个人的意见是接口和方法设计一定要做到职责单一,类的设计尽量做到。