面向对象设计原则之单一职责
大家都知道面向对象是一种编程思想,而面向对象设计(OOD)则可以说是每位程序员都琢磨过的。所谓三人行必有我师,下面看看ohmygodlzl总结的一些OOD的心得体会,其中着重讲述了单一职责这一原则。
一直想跟同志们探讨一下面向对象设计(OOD)的原则问题,但因为自己理解有限,怕说不好误人子弟,一直就没开始。现在想做个尝试,从浅处说起,便于理解,也希望能对我们日常的开发起到帮助。
我们做软件开发,要做的事情无非就是:拿到一份需求,通过一系列步骤把它转化为可运行的系统。这些步骤简单的说就是需求分析DD>面向对象分析(包括领域建模)DD>架构设计DD>详细设计DD>编码DD>测试DD>发布这样的过程,其中架构设计和详细设计中都要用到OOD的一些原则。说起面向对象,一般大家都会想到封装,继承,多态,这是面向对象的特征,还不是原则,我想说的原则是Robert C. Martin在他的《敏捷软件开发-原则模式和实践》中提到的原则。今天聊聊单一职责原则(SRP)。
做开发到现在,相信大家已经听到过这样的说法:"类的职责越单一,越容易重用。"这话怎么理解呢?我举个例子,校验码。设想你现在拿到一个任务就是实现我们系统中的登录模块的校验码功能,你怎么做?我觉得我们大部分开发人员都会做下面这样的设计(当然更坏的是有人干脆写一个工具类,提供一个静态的generateVerifyCode方法):
这个设计有什么问题呢?有接口有实现,貌似够合理了。我们拿实际发生的事来看够不够合理DD我们系统刚上线的时候校验码的实现没有现在这样花哨,那个时候只是白色背景加上一组没有经过扭曲处理的数字;但是后来需求进化了,因为原来的校验码可能会被破译,所以需要对数字进行扭曲,而且不能只产生数字,还要有字母,以增加破译的难度。这时我们怎么办?基于上面的设计,就需要修改VerifyCodeGeneratorImpl类(注意:即使在这种不合理的设计下,修改实现也是不好的做法,倒不如丢弃这个实现,新增一个实现),修改其中产生随机数的代码,并且在生成图片的时候对产生的随机文本添加扭曲处理逻辑。因为需求的变化而修改代码, 这说明原来的设计是不好的,违反了面向对象设计的另外一个原则"开放封闭原则(OCP)",这个原则另外再谈,主要就是说一个类(或者模块)只可以扩展,但不可修改。
细分析可以看出,导致实现类需要做修改的原因是:它承担了两个本应该分离的职责DD产生随机文本和生成校验码图片。好,我们尝试将产生随机文本的职责分离出来,设计如下:
在这样的设计下, 校验码随机文本的生成职责被分离成一个单独的演化体系,随着需求的变化可以添加产生汉字随机文本之类的新实现,并且不影响校验码的显示。但这个设计还没有满足需求的变更,因为现在的需求是不光随机文本内容从纯数字变成了数字加字母,而且要求显示的时候对数字进行扭曲,在这个设计中,我们可以添加新的VerifyCodeGenerator实现如TransformedVerifyCodeGeneratorImpl以替换原来的VerifyCodeGeneratorImpl。这样做可行,但还有更好点的设计,如下:
这样做,把图片的生成职责也单独抽象成一个演化体系,这样以来,将来如果需要在显示校验码时加上背景色或者背景噪音,只需要添加新的ImageGenerator实现。而且ImageGenerator作为一个通用的类,可以被其他有相应生成图片需求的类所重用而不是只局限于生成校验码。上面的设计配合Spring的依赖注入,我们可以生成N * M种校验码(N是RandomTextGenerator的实现类数,M是ImageGenerator的实现类数),而基于最初的设计,我们就需要创建N*M个VerifyCodeGenerator的实现类,且这些实现类可复用性很低。
从上面例子的探讨中可以得出这么一个结论:一个类(或者大到模块,小到方法)承担的职责越多,它被复用的可能性越小。这就是单一职责原则(SRP)要表述的内容:就一个类而言,应该仅有一个引起它变化的原因。
本着这个原则,我们再看一个常见的DAO设计:
interface DAO{ Connection connect(); void close(); void executeUpdate(); ResultSet executeQuery(String sql); }
这个接口有什么问题没有,好像很多人都这么干。只要设想一下如果底层数据库变化了,connect方法的代码就可能需要改变(有人说我们不需要改变,那是因为我们使用了Spring提供的DataSource抽象隔离了取得数据库链接的变化)。这个接口包含了两个职责:数据库链接管理和数据操作。
在实际操作中,如何识别职责是一个说起来容易做起来难的问题,比如有人可能会说,"产生校验码图片"本身就是一个独立职责呀,我说是的,如果我们的校验码图片一成不变,最初的那个设计不算很坏,只不过丧失了一点重用性而已(比如产生随机文本的逻辑可能被其他的模块重用),但是需求后来变化了,不能只显示数字,还要字母,这说明产生随机文本是一个变化纬度,将来很可能还有新的变化,那就应该把这个职责独立出来;需求又说显示的文本需要扭曲,这说明图片的生成也是一个变化纬度,沿着这个纬度将来很可能也有新的变化,那就也应该把这个职责独立出来。需求变化所影响的变化纬度,往往就是应该被独立的职责。所以如果你接到一个需求后发现需要修改一个已经存在的类,那就要考虑一下是不是原来的设计不合理,没有把应该独立出来的职责分离出来。需求变化结合经验、常识,就可以慢慢识别职责应该分到什么粒度了。
单一职责原则不光对类设计有意义,对以模块、子系统为单位的架构设计一样有意义,一个模块、子系统也应该仅有一个引起它变化的原因,不同的是模块和子系统承担的职责粒度跟类相比是另外一个层次了。