概述

“分离职责”是经常使用的一个重构策略,当一个类担任的职责太多时,应按职责将它拆分成多个类,每个类分别承担“单一”的职责,也就是让每个类专心地做“一件事情”。

SRP原则

在面向对象编程中,SRP原则是一个非常重要的原则(SOLID原则都很重要),在展示示例前,我们先了解一下SRP原则是什么,以及它有什么作用。

什么是SRP原则?

SRP原则的定义是这样的:

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

就一个类而言,应该仅有一个引起它变化的原因。

为什么要遵守SRP原则?

When a class has more than one responsibility, there are also more triggers and reasons to change that class. A responsibility is the same as “a reason for change” in this context.

因为每一个职责都是变化的因子,当需求变化时,该变化通常反映为类的职责的变化。
如果一个类承担了多于一个的职责,那么就意味着引起它的变化的原因会有多个,等同于把这些职责耦合在了一起。
一个职责的变化可能会抑制到该类完成其他职责的能力,这样的耦合会导致脆弱的设计。

在设计类或接口时,如果能够遵守SRP原则,则会带来以下优点:

  1. 类的复杂性降低:每个类或接口都只定义单一的职责,定义清晰明确
  2. 可读性提高:定义清晰明确,自然带来较高的代码可读性
  3. 可维护性提高:代码可读性提高,意味着更容易理解;单一职责使得类之间的耦合性较低,更改也会较为容易
  4. 扩展性更好:当需要扩展新的职责时,只需要定义新的接口和新的实现即可

如何遵守SRP原则?

Separating responsibility can be done by defining for every responsibility a class or an interface.

应该为每一项职责定义类或接口的方式实现职责分离。

SRP原则的难点

SRP原则堪称是SOLID原则里面最简单的一个原则,但也可以说是最难的一个。
它的“简单”之处在于它很容易被理解,“困难”之处在于很多人在软件设计过程中,很难真正地抓住关键点。

对于开发者来说,“分离职责”存在四个难点,也是开发者在使用这种重构策略时需要慎重考虑的地方

职责的划分

“职责”单一是相对的,每个人看事情的角度,对业务的理解程度是不尽相同的,这导致了人们对职责的定义和细化程度的差异性。同样一个业务,有些人从角度A出发,在对业务提炼归纳总结后,得出三项职责:J、K、L。而有些人则从角度B出发,归纳总结出两项职责:X、Y。
在设计接口时,这两人自然而然地会设计出不同的接口,两人设计的接口个数和表达的语义也各不相同。

拿装修房子来说,业主通常会委托装修公司来做这件事儿,站在业主的角度理解,它就是一件大事儿——“装修公司装修房子”,至于怎么装修,由装修公司来搞定。
装修公司通常会将这件大事儿拆分成几件小事儿,譬如:“室内设计”、“贴瓷砖”、“做家具”、“刷墙”等等,然后再去雇佣不同类型的工人来完成这些小事儿。

类的命名

职责和类的命名应该匹配,如果在职责归纳时,归纳出的职责比较模糊,可能会使类的命名变得艰难。
另外,即使你归纳出的职责是清晰的,如果命名与职责不符(词不达意),仍然会给将来的维护、再重构带来一些困难(命名是非常非常重要的)。

类粒度的控制

将多个职责被拆分到多个类时,原本在一个类中体现的职责被分散到多个类了,与此同时也需要考虑类的粒度。
粒度应当适中,粒度的控制没有固定的标准,这需要结合业务场景具体分析。

类之间的依赖

原本用一个类就能完成的功能,现在需要结合多个类才能完成。
现在为了确保原有的功能仍然能正常运行,较大可能会形成多个类之间的依赖关系。
如果有些类被迁移到其他工程了,这还会涉及到工程之间的依赖关系。

小结

“分离职责”是比较难的一个重构策略,尤其是在一些大型项目中。
该策略如果不能良好地利用,可能会让你的工程或解决方案变得不伦不类。
如果你的重构经验较浅,建议你从一些较小的项目练习这项重构策略。

示例

重构前

这段代码包含两个类Video和Customer。
Viedo类包含三个职责:支付费用、租借Video和计算租金。
Video的职责太多,它把Customer类的职责也“抢”过来了。

public class Video
{public void PayFee(decimal fee){}public void RendVideo(Video video, Customer customer){customer.Videos.Add(video);}public decimal CalculateBalance(Customer customer){return customer.LateFees.Sum();}
}public class Customer
{public IList<decimal> LateFees { get; set; }public IList<Video> Videos { get; set; }
}

重构后

在归纳职责时,我们可以通过识别主语的方式来确定其归属。

  • 支付费用的主语是“客户”,即“客户支付费用”。
  • 计算租金的主语也是“客户”,即“计算客户的租金”。

所以,我们可以将Video类的PayFee()、CalculateBalance()方法放到Customer类中。

public class Video
{public void RentVideo(Video video, Customer customer){customer.Videos.Add(video);}
}public class Customer
{public IList<decimal> LateFees { get; set; }public IList<Video> Videos { get; set; }public void PayFee(decimal fee){}public decimal CalculateBalance(Customer customer){return customer.LateFees.Sum();}
}

原则就像基准线,在设计类和接口时,我们应该尽量遵守基准线,而不是死守基准线,在设计时不应死板地依照原则进行设计。这就好比开车,司机的视线应该始终保持在正前方,如果沿着公路上的线开车而忽视了前方的交通情况,可能会引发交通事故。

小酌重构系列目录汇总
关注keepfool

转载于:https://www.cnblogs.com/keepfool/p/5484139.html

小酌重构系列[10]——分离职责相关推荐

  1. 路漫漫其修远兮,吾将上下而求索——小酌重构系列[0]开篇有益

    相信博客园的读者大多都是千万"码农"中的一员,每个人都写过很多代码,但并不是每一个人都能写出高质量的代码. rome is not built in one day !--完成高质 ...

  2. 小酌重构系列[19]——分解大括号

    概述 if else, for, while等是程序中最常用的语句,这些语句有一个共同点--它们的逻辑都封装在一对"{}"包围的代码块中.在实现复杂的业务逻辑时,会较多地用到这些语 ...

  3. 小酌重构系列[17]——提取工厂类

    概述 在程序中创建对象,并设置对象的属性,是我们长干的事儿.当创建对象需要大量的重复代码时,代码看起来就不那么优雅了.从类的职责角度出发,业务类既要实现一定的逻辑,还要负责对象的创建,业务类干的事儿也 ...

  4. 小酌重构系列[8]——提取接口

    前言 世间唯一"不变"的是"变化"本身,这句话同样适用于软件设计和开发. 在软件系统中,模块(类.方法)应该依赖于抽象,而不应该依赖于实现. 当需求发生&quo ...

  5. 小酌重构系列[20]——用条件判断代替异常

    小酌重构系列[20]--用条件判断代替异常 参考文章: (1)小酌重构系列[20]--用条件判断代替异常 (2)https://www.cnblogs.com/keepfool/p/5513946.h ...

  6. 小酌重构系列[21]——避免双重否定

    避免双重否定 在自然语言中,双重否定表示肯定.但是在程序中,双重否定会降低代码的可读性,使程序不易理解,容易产生错觉. 人通常是用"正向思维"去理解一件事情的,使用双重否定的判断, ...

  7. 小酌重构系列[18]——重命名

    概述 代码是从命名开始的,我们给类.方法.变量和参数命名,我们也给解决方案.工程.目录命名.在编码时,除了应该遵守编程语言本身的命名规范外,我们应该提供好的命名.好的命名意味着良好的可读性,读你代码的 ...

  8. 小酌重构系列[4]——分解方法

    概述 "分解方法"的思想和前面讲到的"提取方法"."提取方法对象"基本一致. 它是将较大个体的方法不断的拆分,让每个"方法&quo ...

  9. 小酌重构系列[16]——引入契约式设计

    概述 试想这样一个场景,你提供了一些API给客户端调用,客户端传入了一些参数,然后根据这些参数执行了API逻辑,最终返回一个结果给客户端. 在这个场景中,有两个隐患,它们分别是: 客户端调用API时, ...

最新文章

  1. redis持久化的几种方式
  2. ASP.NET Core 中文文档 第四章 MVC(4.2)控制器操作的路由
  3. zookeeper做集群后启动不了,大部分原因是防火墙未关闭
  4. Java web表单异步提交,javaweb系统,我的电脑浏览器可以正常异步提交操作参数给后台,但是同事电脑今天却不知道怎么了,提交给后台的参数为空...
  5. 数学奥赛大神,两次以满分获IMO金牌,北大数学博士提前毕业
  6. Adobe Illustrator CS6 绿色简体中文版下载地址
  7. [转载] 七龙珠第一部——第112话 能恢复年轻吗 比克大魔王
  8. 高阶篇:4.2)DFMEA设计失效模式和失效后果分析-总章
  9. linux中pak命令,Linux常用包管理及命令
  10. Motion 5 for Mac(专业视频编辑软件)v5.3.2永久破解版
  11. shell 小米system锁adb_小米/红米系列手机解system分区锁方法详解
  12. PPT实现倒计时功能(VBA实现)
  13. Qt——多语言程序设计
  14. CruiseControl 安装和启动
  15. HTML5游戏引擎(八)-矢量绘图——绘制矩形-drawRect 绘制圆形-drawCircle 绘制直线-moveTo和 lineTo
  16. C++ 万年历、生肖判断、计算第几天
  17. Enterprise Architect 15 使用指南
  18. date日期格式 yyyy-MM-dd HH:mm:ss 大小写区别
  19. JPEG文件中的EXIF(下)
  20. 工程图学及计算机绘图宋卫卫,工程图学及计算机绘图习题集

热门文章

  1. Jmeter正则表达式提取器的使用
  2. 服务器如何防御攻击?
  3. Noip—p1309 瑞士轮
  4. 通过python-pptx模块操作ppt文件
  5. twisted的cred
  6. 基于js利用经纬度进行两地的距离计算
  7. HEVC之tiles、slice、slice segment、CU、PU、TU分析
  8. TMMi连接传统与敏捷--2017中国首届TMMi国际峰会在京圆满举行
  9. 华南理工计算机应用在线答题,华南理工大学计算机应用基础随堂练习题目及答案...
  10. java-UTC时间戳格式化成年月日,UTC时间戳转成北京时间并格式化年月日