前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >单一职责原则(Single Responsibility Principle,SRP)

单一职责原则(Single Responsibility Principle,SRP)

作者头像
JavaEdge
发布2021-02-22 16:42:16
5340
发布2021-02-22 16:42:16
举报
文章被收录于专栏:JavaEdge

1 简介

1.1 定义

不要存在多于一个导致类变更的原因。该原则备受争议,争议之处在于对职责的定义,什么是类的职责?怎么划分类的职责?

1.2 特点

一个类/接口/方法只负责一项职责。

1.3 优点

降低类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险。

2 代码实战

2.1 鸟类案例

  • 最开始的 Bird 类
  • 测试类
简单测试类
简单测试类

显然鸵鸟用翅膀飞是错误的!

  • 修改类实现
  • 以上的设计依旧很差,总不能一味堆砌 ifelse 添加鸟类,结合该业务逻辑,考虑分别实现类职责,即根据单一原则创建两种鸟类即可。

2.2 课程案例

  • 最初的课程接口有两个职责,耦合过大
  • 按职责拆分

3 终极实例

3.1 单一!!!

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

  • 用户信息维护类图

得有问题,用户的属性和用户的行为没有分开,这是一个严重的错误! 这个接口确实设计得一团糟

  • 应该把用户的信息抽取成一个BO(Business Object,业务对象)
  • 把行为抽取成一个Biz(Business Logic,业务逻辑)
  • 职责划分后的类图

重新拆封成两个接口

  • IUserBO负责用户的属性,简单地说,IUserBO的职责就是收集和反馈用户的属性信息
  • IUserBiz负责用户的行为,完成用户信息的维护和变更

看一看分拆成两个接口怎么使用。我们现在是面向接口编程,所以产生了这个UserInfo对象之后,当然可以把它当IUserBO接口使用。也可以当IUserBiz接口使用,这要看你在什么地方使用了。

  • 要获得用户信息,就当是IUserBO的实现类
  • 要是希望维护用户的信息,就把它当作IUserBiz的实现类就成
代码语言:javascript
复制
IUserInfo userInfo = new UserInfo();
//我要赋值了,我就认为它是一个纯粹的BO
IUserBO userBO = (IUserBO)userInfo;
userBO.setPassword("abc");
//我要执行动作了,我就认为是一个业务逻辑类
IUserBiz userBiz = (IUserBiz)userInfo;
userBiz.deleteUser();

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

项目中经常采用的SRP类图
项目中经常采用的SRP类图

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

3.2 打破你的传统思维

  • SRP There should never be more than one reason for a class to change.

再来看看下面这个例子是否好理解。电话通话的时候有4个过程发生:拨号、通话、回应、挂机 那我们写一个接口

电话类图
电话类图
  • 电话过程
代码语言:javascript
复制
public interface IPhone {
     //拨通电话
     public void dial(String phoneNumber);
     //通话
     public void chat(Object o);
     //通话完毕,挂电话
     public void hangup();
}

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

IPhone这个接口可不是只有一个职责,它包含了两个职责:

  • 协议管理 dial()和hangup()两个方法实现的是协议管理,分别负责拨号接通和挂机
  • 数据传送 chat()实现的是数据的传送,把我们说的话转换成模拟信号或数字信号传递到对方,然后再把对方传递过来的信号还原成我们听得懂的语言。

协议接通的变化会引起这个接口或实现类的变化吗?会的!那数据传送(想想看,电话不仅仅可以通话,还可以上网)的变化会引起这个接口或实现类的变化吗?会的!那就很简单了,这里有两个原因都引起了类的变化。这两个职责会相互影响吗?电话拨号,我只要能接通就成,甭管是电信的还是网通的协议;电话连接后还关心传递的是什么数据吗? 通过这样的分析,我们发现类图上的IPhone接口包含了两个职责,而且这两个职责的变化不相互影响,那就考虑拆分成两个接口

职责分明的电话类图
职责分明的电话类图

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

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

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

好处

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

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

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

4 我单纯,所以我快乐

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

单一职责原则很难在项目中得到体现,非常难,为什么? 在国内,技术人员的地位和话语权都比较低,因此在项目中需要考虑环境,考虑工作量,考虑人员的技术水平,考虑硬件的资源情况,等等,最终妥协的结果是经常违背单一职责原则。而且,我们中华文明就有很多属于混合型的产物,比如筷子,我们可以把筷子当做刀来使用,分割食物;还可以当叉使用,把食物从盘子中移动到口中。而在西方的文化中,刀就是刀,叉就是叉,你去吃西餐的时候这两样肯定都是有的,刀就是切割食物,叉就是固定食物或者移动食物,分工很明晰。这种文化的差异很难一步改造过来,但是我相信随着技术的深入,单一职责原则必然会深入到项目的设计中,而且这个原则是那么的简单,简单得不需要我们更加深入地思考,单从字面上大家都应该知道是什么意思,单一职责嘛!

单一职责适用于接口、类,同时也适用于方法。一个方法尽可能做一件事情,比如一个方法修改用户密码,不要把这个方法放到“修改用户信息”方法中,这个方法的颗粒度很粗

  • 一个方法承担多个职责

在IUserManager中定义了一个方法changeUser,根据传递的类型不同,把可变长度参数changeOptions修改到userBO这个对象上,并调用持久层的方法保存到数据库中。 在我的项目组中,如果有人写了这样一个方法,我不管他写了多少程序,花了多少工夫,一律重写! 原因很简单:方法职责不清晰,不单一,不要让别人猜测这个方法可能是用来处理什么逻辑的。比较好的设计如下

  • 一个方法承担一个职责

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

5 最佳实践

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

参考

  • 《设计模式之蝉》
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/10/11 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 简介
    • 1.1 定义
      • 1.2 特点
        • 1.3 优点
        • 2 代码实战
          • 2.1 鸟类案例
            • 2.2 课程案例
            • 3 终极实例
              • 3.1 单一!!!
                • 3.2 打破你的传统思维
                  • 好处
                  • 4 我单纯,所以我快乐
                  • 5 最佳实践
                  相关产品与服务
                  访问管理
                  访问管理(Cloud Access Management,CAM)可以帮助您安全、便捷地管理对腾讯云服务和资源的访问。您可以使用CAM创建子用户、用户组和角色,并通过策略控制其访问范围。CAM支持用户和角色SSO能力,您可以根据具体管理场景针对性设置企业内用户和腾讯云的互通能力。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档