本文主要是介绍大旗不挥,谁敢冲锋—六大设计原则之单一职责原则,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
单一职责原则
1. 我是“牛”类,我可以担任多职吗
单一职责原则,英文名称是:Single Responsibility Principle,简称SRP。该职责备受争议,争议之处在于——对职责的定义,什么是类的职责,以及怎么划分类的职责。但是首先弄清楚什么是单一职责原则?
在做项目的时,用户、机构、角色管理这些模块肯定会接触到,基本上使用的都是RBAC模型(Role-Based Access Control,基于角色的访问控制,通过分配和取消角色来完成用户权限的授予和取消,使动作主体【用户】与资源的行为【权限】分离)
对于用户管理、修改用户的信息、增加机构(一个人属于多个机构)、增加角色等,用户有这么多的信息和行为要维护,于是将这些写到一个接口中,反正都是用户管理类嘛,看它的类图,如图1-1所示:
图 1-1 用户信息维护类图
如上图所示,显然这个接口设计得有问题——用户的属性和行为没有分开,这是一个严重的错误!正确的做法应该是把用户的信息抽取成一个BO(Business Object,业务对象),把行为抽取成一个Biz(Business Logic,业务逻辑),按照这个思路对类图进行修正,如图1-2所示:
图 1-2 职责拆分后的类图
拆分成两个接口,IUserBO负责用户属性(收集和反馈用户的属性信息);IUserBiz负责用户的行为(完成用户信息的维护和变更)。采用面向接口编程,所以产生了UserInfo对象之后,当然可以把它当作IUseBO接口使用。也可以当作IUserBiz接口使用,这就决定于具体的业务场景。如果希望获得用户信息,就当是IUserBO的实现类;要是希望维护用户的信息,就把它当作IUserBiz的实现类,例如代码1-1所示:
代码清单1-1 分清职责后的代码实例
......
IUserInfo userInfo = new IUserInfo();
// 我要赋值了,我就认为它是纯粹的BO
IUserBO userBO = (IUserBo)userInfo;
userBO.setPassword("123456");
// 我要执行一个动作了,我就认为是一个业务逻辑类
IUserBiz userBiz = (IuserBiz)userInfo;
userBiz.deleteUser();
......
这样问题解决了,但是经过分析刚才的动作,为什么要将一个接口拆分成两个呢?其实,在实际使用中,更倾向于使用两个不同的类或接口;一个是IUserBO,一个是IUserBiz,leu如如图1-3所示:
图1-3 项目中经常采用的SRP类图
上面将接口拆分成两个接口的动作,就是依赖了单一职责原则,那什么是单一职责原则呢?单一职责原则定义是:应该有且仅有一个原因引起类的变更。
2. 绝技,打破传统思维
SRP的原话解释:There should never be more than one reason for a class to change
再举一个电话的例子,再电话通话的时候有4个过程发生:拨号、通话、回应、挂机,写一个接口,其类图如1-4所示:
图 1-4 电话类图
代码如清单1-2所示:
public interface IPhone {// 接通电话void dial(String phoneNumber);// 通话void chat(Object o);// 通话完毕,接电话void hangup();
}
上面代码清单中的IPhone接口,接近“完美”!单一职责原则要求一个接口或类只有一个原因引起变化,也就是一个接口或类只有一个职责
,它就负责一件事情,但是上面的接口只负责一件事情吗?是只有一个原因引起变化吗?好像不是!
IPhone这个接口可不是只有一个职责,它包含了两个职责:一个协议管理,一个是数据传送。dial()和hangup()两个方法实现的是协议管理,分别负责拨号接通和挂机;chat()实现的是数据的传送,把说的话转换成模拟信号或数字信号传递到对方,然后再把对方传递过来的信号还原成我们听得懂的语言。
协议接通的变化会引起这个接口或实现类的变化吗?会的!那数据传送(电话不仅仅可以通话,还可以上网)的变化会引起这个接口或实现类的变化吗?也会的!因此,这两个原因都引起了累的变化!。这两个职责会相互影响吗?电话拨号,只要能够接通就成,甭管是电信的还是网通的协议;电话连接后还关心传递的是什么数据吗?通过这样的分析,我们发现类图上的IPhone接口包含了两个职责,并且这两个职责的变化互不影响,那就考虑拆分成两个接口,其类图如图1-5所示:
图 1-5 职责分明的电话类图
这个类图看上去有点复杂,但完全满足了单一职责原则的要求,每个接口职责分明,结构清晰,但是在设计的时候肯定不会采用这种方式
。一个手机类要把ConnectionManager和DataTransfer组合在一块才能使用。组合是一种强耦合关系,你和我都有共同的生命周期,这样的强耦合关系还不如使用接口实现的方式呢?而且还增加了类的复杂性,多了两个类。经过这样的考虑,修改了一下类图,如图1-6所示:
图1-6 简介清晰、职责分明的电话类图
这样的设计才是完美的,一个类实现了两个接口,把两个职责融合在一个类中。不过会发现其中Phone有两个原因引起变化了呀,是的,但由于采用的是面向接口编程,对外公布的是接口而不是实现类。而且,如果真要实现类的单一职责,这个就必须使用上面的组合模式了,这会引起类间的耦合过重、类的数量增加等问题,人为的增加了设计的复杂性。这样会得不偿失!
通过上面的例子,可以总结一下单一职责原则的好处:
- 类的复杂性降低。实现什么职责都有清晰明确的定义
- 可读性提高。复杂性降低,当然可读性提高了
- 可维护性提高。可读性提高,当然更容易维护了
- 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做的很好,一个接口修改只对相应的实现类有影响,对其他接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
虽然单一职责原则有上面的好处,但是单一职责的“职责”怎么划分。一个职责一个接口,问题是“职责”没有一个量化的标准,一个类到底要负责哪些职责?这些职责该怎么细化?细化后是否都要一个接口或类?这些都需要从实际项目去考虑,从功能上说,定义一个IPhone接口也没有错,实现了电话的功能,并且设计简单,仅仅一个接口和一个实现类,在实际项目中也会这样设计。项目要考虑可变因素和不可变因素,以及相关的收益成本比率,因此设计一个IPhone接口也可能是没有错的。但是,如果纯从“学究”理论上分析就有问题了,有两个可以变化的原因放到了一个接口中,这就为以后的变化带来了风险。如果电话升级到了数字电话,我们提供的接口IPhone是不是要修改了?接口修改对其他的Invoker类是不是有很大的影响?
注意:单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计的是否优良,但是“职责”和“变化原因”都是不可以度量的,因项目而异,因环境而异。
3. 我单纯,我快乐
对于接口,在设计的时候一定要做到单一,但是对于实现类就需要多方面的考虑了。生搬硬套单一职责原则会引起类的剧增,给维护带来非常多的麻烦,而且过分细分类的职责也会人为地增加系统的复杂性。本来一个类可以实现的行为硬要拆成两个类,然后在使用聚合或组合的方式耦合在一起,人为制造了系统的复杂性。所以原则是死的,人是活的。
单一职责原则很难在项目中得到体现,非常难。原因是,在国内,技术人员的地位和话语权比较低(深有体会),因此在项目中需要考虑环境、考虑工作量、考虑人员的技术水平,考虑硬件的资源情况,等等,最终妥协的结果是常常违背单一职责原则。然而,我坚信随着技术的深入,单一职责必然会深入到项目设计中,而且这个原则是那么简单!
单一职责适用于接口、类,同时也使用于方法。就是,一个方法尽可能做一件事情,比如一个方法修改用户密码,不要把这个方法放到“修改用户信息”方法中,这个方法的颗粒度很粗,比如图1-7中所示的方法。
图 1-7 一个方法承担多个职责
在IUserManager中定义了一个方法changeUser,根据传递的类型不同,把可变长度参数changeOptions修改到了userBO这个对象上,并调用持久层的方法保存到数据库中。这种方法书写的非常糟糕,原因是:方法职责不清晰,不单一,不要让别人猜测这个方法可能是用来处理什么逻辑,比较好的设计如图1-8所示:
图 1-8 一个方法承担一个职责
通过类图可以知道,如果修改用户名称,就调用changeUserName方法;要修改家庭地址,就调用changeHomeAddress方法;要修改单位电话,就调用changeOfficeTel。每个方法的职责非常清晰明确,不仅有利于开发,也为日后的维护带来遍历。要逐渐养成这样的习惯。
所以,如果对接口、类和方法使用了单一职责原则,会带来很大的便利的。
4. 最佳实践
类的单一职责原则在运用中受非常多因素的制约,纯理论来讲,这个原则非常的优秀,但现实有现实的难处,必须去考虑项目工期、成本、人员技术水平、硬件情况、网络情况甚至有时候还需要政府政策、垄断协议等因素。
总之,对于单一职责原则,接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。
这篇关于大旗不挥,谁敢冲锋—六大设计原则之单一职责原则的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!