六大设计原则——单一职责原则

单一职责原则的英文名称是Single Responsibility Principle,简称是SRP。这个设计原则备受争议,争议之处就是对职责的定义,什么是类的职责,以及怎么划分类的职责。我们先举例来说明什么是单一职责原则。

基本上所有接触到用户、机构、角色管理这些模块的项目,使用的都是RBAC模型(Role-Based Access Control,基于角色的访问控制,通过分配和取消角色来完成用户权限的授予和取消,使动作主体(用户)与资源的行为(权限)分离),确实是一个很好的解决办法。我们这里要讲的是用户管理、修改用户的信息、增加机构(一个人属于多个机构)、增加角色等。用户有这么多的信息和行为要维护,我们就把这些写道一个接口中,都是用户管理类嘛,我们来看看它的类图,如图1-1所示。

图1-1​​ 用户信息维护类图

 

如此简单的类图,即便是一个初级的程序员也可以看出这个接口设计的有问题,用户的属性和用户的行为没有分开,这是一个严重的错误。这个接口确实设计的一团糟,应该把用户的信息抽取成一个BO(Business Object,业务对象),把行为抽取成一个Biz(Business Logic,业务逻辑),按照这个思路对类图进行修正,如图1-2所示。

图1-2 职责划分后的类图

重新拆封成两个接口,IUserBO负责用户的属性,简单的说,IUserBO的职责就是收集和反馈用户的属性信息;IUserBiz负责用户的行为,完成用户信息的维护和变更。这个其实与我们实际工作中用到的User类还是有差别的,不用着急,我们先来看一看分拆成两个接口怎么使用。我们现在是面向对象编程,所以产生了这个UserInfo对象之后,当然可以把它当IUserBO接口使用。也可以当IUserBiz接口使用,这要看你在什么地方用了。要获得用户信息,就当是IUserBO的实现类;要是希望维护用户信息,就把它当作IUserBiz的实现类就成了,如下代码所示。

……
IUserInfo userInfo = new UserInfo();
//我要赋值了,我就认为它是一个纯粹的BO
IUserBO userBo = (IUserBo) userInfo;
userBo.setPassword("abc");
//我要执行动作了,我就认为这是一个业务逻辑类
IUserBiz userBiz = (IUserBiz) userInfo;
userBiz.deleteUser();
……

确实可以如此,问题也解决了,但我们分析一下刚才的动作,为什么要把一个接口拆分成两个呢?其实,在实际的使用中,我们更倾向于使用两个类或接口:一个是IUserBO,一个是IUserBiz,类图如图1-3所示。

图1-3 项目中经常采用的SRP类图

 

以上我们把一个接口拆分成两个接口的动作,就是依赖了单一职责原则,那么什么是单一职责原则呢?单一职责原则的定义是:应该有且仅有一个原因引起类的变更

SRP的原话解释是:There should never be more than one reason for a class to change.

上面的例子很好理解,在实际的项目中大家都已经这么做了,那么我们在来看看下面的这个例子是否好理解。电话,是现代人都离开不了的,电话通话的时候有4个过程发生:拨号,通话,回应,挂机。那我们写一个接口,其类图如图1-4所示。

图1-4 电话类图

 

我们来看一下过程的代码,如下代码所示。

public interface IPhone{
    //拨通电话
    public void dial(String phoneNumber);
    //通话
    public void chat(Object o);
    //通话完毕,挂电话
    public void hangup();
}

实现类也比较简单,我就不再写了,大家看看这个接口有没有问题?我相信大部分的读者都会说这个没有问题呀,是的,这个接口接近于完美,看清楚了,是“接近”!单一职责原则要求一个接口或类只有一个原因引起变化,也就是一个接口或类只有一个职责,它就负责一件事情,看看上面的接口只负责一件事情吗?是只有一个原因引起变化吗?好像不是!

IPhone这个接口可不是只有一个职责,它包含了两个职责:一个是协议管理,一个是数据传送。dial()和hangup()两个方法实现的是协议管理,分别负责拨号接通和挂机;chat()实现的是数据的传送,把我们说的话转换成模拟信号或数字信号传递给对方,然后再把对方传递过来的信号还原成我们听得懂的语言。我们可以这样考虑这个问题,协议接通的变化会引起这个接口或者实现类的变化吗?会的!那数据传送(想想看,电话不仅仅可以通话,还可以上网)的变化会引起这个接口或者实现类的变化吗?会的!那就很简单了,这里有两个原因都引起了类的变化。这两个职责会互相影响吗?电话拨号,我只要能接通就行,不管他是电信的还是网通、移动的协议;电话接通后还关心传送的是什么数据吗?通过这样的分析,我们发现类图上的IPhone接口包含了两种职责,而且这两种职责的变化不互相影响,那就考虑分成两个接口,其类图如图1-5所示。

图1-5 职责分明的电话类图

 

这个类图看起来有点复杂了,完全满足了单一职责原则的要求,每个接口职责分明,结构清晰,但我们在设计的时候通常不会参用这种方式,一个手机类要把ConnectionManager和DataTransfer组合在一起才能使用。组合是一种强耦合关系,你和我都有共同的生命周期,这样的强耦合关系还不如使用接口实现的方式呢,而且还增加了类的复杂性,多了两个类,经过这样的思考,我们在修改一下类图,如图1-6所示。

图1-6 简洁清晰、职责分明的电话类图

这样的设计才是完美的,一个类实现了两个接口,把两个职责融合在一个类中。你会觉得这个Phone有两个原因引起变化了呀,是的,但是别忘了我们是面向接口编程,我们对外公布的是接口而不是实现类。而且,如果真要实现类的单一职责,这个就必须使用上面的组合模式了,这会引起类间耦合过重、类的数量增加等问题,人为地增加了设计的复杂性。

通过上面的例子我们总结一下单一职责原则有什么好处:

  • 类的复杂性降低,实现什么职责都有清晰明确的定义;
  • 可读性提高,复杂性降低,那当然可读性提高了;
  • 可维护性提高,可读性提高,那当然更容易维护;
  • 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做的好,一个接口修改只对相应实现类有影响,对其他接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

看过电话这个例子后,是不是想反思一下了,我以前的设计是不是有点问题?不,不是的,不要怀疑自己的技术能力,单一职责原则最难划分的就是职责。一个职责一个接口,但问题是职责没有一个量化的标准,一个类到底要负责那些职责?这些职责该怎么细化?细化后是否都要有一个接口或类?这些都需要从实际的项目去考虑,从功能上来说,定义一个IPhone类也没有错,实现了电话的功能,而且设计还非常简单,仅仅一个接口不一个实现类,实际的项目我想大家都会这么设计。项目要考虑可变因素和不可变因素,以及相关的收益成本比率,因此设计一个IPhone接口也可能是没有错的。但是,如果纯从“学究”理论上分析就有问题了,有两个可以变化的原因放到了一个接口中,这就为以后的变化带来了风险如果以后模拟电话升级到了数字电话,我们提供的接口IPhone是不是要修改了?接口修改对其他的Invoker类是不是有很大影响?

注意:单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计的是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。

对于接口,我们在设计的时候一定要做到单一,但是对于实现类就需要多方面的考虑了。生搬硬套单一职责原则会引起类的剧增,给维护带来非常多的麻烦,而且过分细分类的职责也会人为地增加系统的复杂性。本来一个类可以实现的行为硬要拆分成两个类,然后在使用聚合或者组合的方式耦合在一起,人为制造了系统的复杂性。所以,原则是死的,人是活的,这句话很有道理。

单一职责适用于接口,类,同时也适用于方法,什么意思呢?一个方法尽可能去做一件事情,比如一个方法修改用户密码,不要把这个方法放在“修改用户信息”方法中,这个方法的颗粒度很粗,比如图1-7所示的方法。

图1-7 一个方法承担多个职责

在IUserManager中定义了一个方法changeUser,根据传递的类型不同,把可变长度参数changeOptions修改到userBO这个对象上,并调用持久层的方法保存到数据库中。这样的方法,职责不清晰,不单一。不要让别人猜测这个方法可能是处理什么逻辑的。比较好的设计如图1-8所示。

图1-8 一个方法承担一个职责

通过类图可知,如果要修改用户名称,就要调用changeUserName方法;要修改家庭地址,就调用changeHomeAddress方法;要修改单位电话,就调用changeOfficeTel方法。每个方法的职责非常清晰明确,不仅开发简单,而且日后的维护也非常简单。

阅读到这里,可能有人想问,写的是类的设计原则吗?你通篇都在说接口的单一职责,类的单一职责你都违背了呀!这个还真是的,我的本意是把这个原则说清楚,类的单一职责嘛,这个很简单,但当回头写的时候,发觉并不是这么回事,翻看了以前的代码和设计,基本上拿得出手的类设计都是与单一职责违背的。静下心来回忆,发觉每一个类这样设计都是有原因的。Wikipedia、OODesign等几个网站,专家也有类似的经验,基本上类的单一职责都用了类似一句话来说“This is sometimes hard to see”,这句话翻译过来就是“这个有时候很难说”。是的,类的单一职责确实受非常多的因素制约,纯理论上来讲,这个原则是非常优秀的,但现实又现实的难处,你必须考虑项目的工期、成本、人员技术水平、硬件情况、网络情况甚至还要考虑政府政策、垄断协议等因素。

对于单一职责原则,建议接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值