zoukankan      html  css  js  c++  java
  • Java 设计模式

      单一职责原则的英文名称是 Single Responsibility Principle,简称是 SRP。

      这个原则因为它对职责的定义,什么是类的职责,以及怎么划分类的职责,而备受争议。

      只要做过项目,肯定要接触到用户、机构、角色管理这些模块,基本上使用的都是 RBAC 模型(Role-Based Access Control,基于角色的访问控制,通过分配和取消角色来完成用户权限的授予与取消,使动作主体(用户)和资源的行为(权限)分离),确实是一个很好的解决办法。

      


      来看一个类图:

      

      这个类图,很容易可以看出问题所在:用户的属性和用户的行为没有分开,这是一个特别严重的错误。

      按照把用户的信息抽取成一个 BO(Business Object,业务对象),把用户的行为抽取成一个 Biz(Business Logic,业务逻辑)的思路对上面的类图进行修正,得如下类图:

      

      重新拆分成两个接口,IUserBO 负责用户的属性(set/get),简单地说,IUserBO 的职责就是收集和反馈用户的属性信息;IUserBiz 负责用户的行为,完成用户信息的维护和变更。

      Java 中接口是允许多继承的,那么,IUserInfo 就拥有 IUserBO 和 IUserBiz 的所有抽象方法,还包括自己的抽象方法,而 Java 多态还能让我们通过强制类型转换,根据具体的情景将同一个 IUserInfo 的对象转换成 IUserBO 或者 IUserBiz 对象进行操作:

      

    1 ... ...
    2 IUserInfo userInfo = new UserInfo();
    3 // 当我们要对它进行赋值操作时,就视它为一个纯粹的 BO 对象
    4 IUserBO userBO = (IUserBO) userInfo();
    5 userBO.setPassword("123456");
    6 // 当我们要对它进行业务行为时,就视它为一个纯粹的 Biz 对象
    7 IUserBiz userBiz = (IUserBiz) userInfo();
    8 userBiz.deleteUser();
    9 ... ...

      确实可以如此,问题解决了,让我们来分析一下刚才的操作,为什么要把一个接口拆分成两个呢?其实,在实际的使用中,我们更加倾向于使用两个不同的类或接口:一个是 IUserBO,另一个是 IUserBiz,各自有各自的实现类 UserBO 和 UserBiz,如图:

      

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


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

      这句话初中生都能看懂,不多说,看懂是一回事,做出来就是另外一回事了,上面的例子很好理解,在实际项目中大家都已经这么做了,那我们再来看看下面这个例子怎么理解:

      电话这玩意儿,如今每个人都离不开,电话通话的时候有 4 个过程发生:拨号、通话、回应和挂机,我们写一个接口,类图如下:

      

      接下来模拟一个打电话的过程:

      

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

      这个接口看上去“近乎”完美,但仔细看并分析,发现它并没有遵守 SRP 原则,这个接口的职责并不单一!

      上面的 IPhone 接口包含了两个职责:一个是协议管理,一个是数据传送,dial() 和 hangup() 实现的是协议管理,分别负责拨号接通和挂断;chat() 实现的是数据的传送,把我们说的话转换成模拟信号或数字信号传递到对方,然后再把传送过来的信号转换成声音。

      我们来考虑一个问题:协议管理的变化会引起这个接口或实现类的变化吗?拿数据传送的变化会引起这个接口或实现类的变化吗?答案是:都会!

      我们再来考虑一个问题:这两个职责会相互影响吗?电话拨号,我们只管拨没拨通,甭管是电信还是网通的协议;电话连接后还关心传递的是什么数据吗?

      通过上面两个问题的分析,我们发现类图上的 IPhone 接口包含了两个职责,而且这两个职责的变化互不影响,那就考虑拆分成两个接口,其类图如下:

      

      这个类图看上有点复杂,完全满足了单一职责原则的要求,每个接口职责分明,结构清晰,但是我相信你在设计的时候肯定不会采用这种方式,一个收集类要把 ConnectionManager 和 DataTransfer 组合在一起才能使用,组合是一种强耦合关系,你和我都有共同的生命周期,这样的强耦合关系用接口实现的方式替代会更好,接口实现的方式还不会多了两个类,增加类的复杂性,经过思考,得如下类图:

    或者直接一个实现类实现两个接口:

      

      这样的设计才是完美的,一个类实现了两个接口,把两个职责融合在一个类中。

      你可能会觉得这样的话 Phone 类就有两个原因引起变化了,没错,但是我们是面向接口编程,我们对外公布的是接口而不是实现类,接口的职责保持单一即可。

      如果真的要实现类的单一职责,就必须使用上面说过的组合模式,但这会引起类间耦合过重、类的数量增加等问题,人为地增加了设计地复杂性。

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

    • 类的复杂性降低了,实现什么职责都可以在接口中有明确清晰的定义;
    • 复杂性降低,当然可读性就提高了;
    • 可读性提高,当然就更容易维护了;
    • 变更引起的风险降低。

      变更是不可避免的,如果 SRP 原则做得好,一个接口修改只对相应得实现类有影响,对其他接口无影响,这对系统的可扩展性、维护性都有非常大的帮助。


      看过电话这个例子后,是不是想反思一下:我以前的设计是不是有点问题了?

      不,不是的,不要怀疑自己的技术能力,单一职责原则最难划分的就是职责。

      一个职责一个接口,但问题是“职责”没有一个量化的标准,一个类到底要负责哪些职责?这些职责该怎么细化?细化后是否都要有一个接口或类?

      这些都需要从实际的项目去考虑,从功能上来说,定义一个 IPhone 接口也没有错,实现了电话的功能,而且设计还很简单,仅仅一个接口一个实现类,实际的项目我想大家都会这么设计。

      项目要考虑可变因素和不可变因素,以及相关的收益成本比率,因此设计一个 IPhone 接口也可能是没有错的,但是如果纯从“学究”理论上分析就有问题了,有两个变化的原因放到了一个接口中,这就为以后的变化带来了风险。

      如果以后模拟电话升级到数字电话(现实是早就实现了),我们提供的接口 IPhone 是不是就要修改了?接口修改对其他的 Invoker 类是不是有很大的影响?

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


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

      所以,原则是死的,人是活的,这句话很有道理。

      单一职责原则很难在项目中得到体现,非常难,为什么?

      在国内,技术人员的地位和话语权都比较低,因此在项目中需要考虑环境,考虑工作量,考虑人员的技术水平,考虑硬件的资源情况,等等,最终妥协的结果是经常违背 SRP 原则。

      而且,我们中华文明就有很多属于混合型的产物,比如筷子,我们可以把筷子当作刀来使用,分割食物;还可以当作叉使用,把食物从盘子中移动到口中。

      而在西方文化中,刀是刀,叉是叉,你去吃西餐的时候这两样肯定是都有的,刀就是切割食物,叉就是固定食物或者移动食物,分工很明确。


      SRP 适用于接口、类,同时也适用于方法,什么意思呢?一个方法尽可能做一件事情,比如一个方法修改用户密码,不要把这个方法放到“修改用户信息”方法中,这个“修改用户信息”方法的颗粒度很粗,如图:

      

      在 IUserManager 中定义了一个方法 changeUser,根据传递的类型不同,把可变长度参数 changeOptions 修改到 userBO 这个对象上,并调用持久层的方法保存到数据库中。

      在我的项目组中,如果有人写了这样一个方法,我不管他写了多少程序,花了多少功夫,一律重写!

      原因很简单:方法职责不清晰,不单一,不要让别人猜测这个方法可能使用来处理什么逻辑的,比较好的设计如图:

      

      通过类图可知,如果要修改用户名称,就调用 changeUserName 方法;要修改家庭地址,就调用 changeHomeAddress 方法;要修改单位电话,就调用 changeOfficeTel 方法。

      每个方法的职责非常清晰,不仅开发简单,而且日后的维护也非常容易,大家可以逐渐养成这样的习惯。


      “阅读到这里,可能有人会问我,你写的是类的设计原则吗?通篇都在说接口的单一职责,类的单一职责你都违背了呀!

      这个还真是,我的本意是想把这个原则讲清楚,类的单一职责嘛,这个很简单,但当我回头写的时候,发现并不是这么回事,翻看了以前的一些设计和代码,基本上拿得出手的类设计都是与单一职责相违背的。

      静下心来回忆,发觉每一个类这样设计都是有原因的。

      我查阅了 WikiPedia、OODesign 等几个网站,专家和我也有类似的经验,基本上类的单一职责都用了类似的一句话来说:

      ‘This is sometimes hard to see',这句话翻译过来就是讲:这个有时候很难说。

      是的,类的单一职责确实受非常多的因素制约,纯理论来讲,这个原则是非常优秀的,但是现实有现实的难处,你必须去考虑项目的工期、成本、人员技术水平、硬件情况、网络情况甚至有时候还要考虑政府政策、垄断协议等因素。

      比如,2004 年我做过一个项目,做加密处理的,甲方就甩过来一句话,你什么都不用管,调用这个 API 就可以了,不用考虑什么传输协议、异常处理、安全连接等。所以我们就直接使用了 JNI 与加密厂商提供的 API 通信,什么单一职责原则,根本就不用考虑,因为对方不公布通信接口和异常判断。

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

      

                                                                              ----摘自《设计模式之禅》 2020.06.17

  • 相关阅读:
    bzoj 3527: [Zjoi2014]力
    bzoj 1797: [Ahoi2009]Mincut 最小割
    bzoj 1028: [JSOI2007]麻将
    bzoj 1019: [SHOI2008]汉诺塔
    bzoj 1023: [SHOI2008]cactus仙人掌图
    bzoj 3289: Mato的文件管理
    bzoj 4034: [HAOI2015]T2
    bzoj 1218: [HNOI2003]激光炸弹
    bzoj 2431: [HAOI2009]逆序对数列
    The full stack trace of the root cause is available in the Apache Tomcat/8.0.8 logs.
  • 原文地址:https://www.cnblogs.com/taiyo/p/13150965.html
Copyright © 2011-2022 走看看