本文详解设计原则中的单一职责原则,目的还是提高代码的可读性、可扩展性、复用性、可维护性等。

目录

  • 1. 单一职责原则(SRP)

  • 2. 如何理解单一职责原则?

  • 3. 如何判断类的职责是否足够单一?

  • 4. 类的职责是否设计得越单一越好?

经典设计原则包括,SOLID、KISS、YAGNI、DRY、LOD 等。这些设计原则,从字面上理解,都不难。你一看就感觉懂了,一看就感觉掌握了,但真的用到项目中的时候,你会发现,“看懂”和“会用”是两回事,而“用好”更是难上加难。从我之前的工作经历来看,很多同事因为对这些原则理解得不够透彻,导致在使用的时候过于教条主义,拿原则当真理,生搬硬套,适得其反。

1. 单一职责原则(SRP)

刚才我们提到了 SOLID 原则,实际上,SOLID 原则并非单纯的 1 个原则,而是由 5 个设计原则组成的,它们分别是:单一职责原则、开闭原则、里式替换原则、接口隔离原则和依赖反转原则,依次对应 SOLID 中的 S、O、L、I、D 这 5 个英文字母。我们今天要学习的是 SOLID 原则中的第一个原则:单一职责原则。

2. 如何理解单一职责原则?

单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。这个原则的英文描述是这样的:A class or module should have a single responsibility。如果我们把它翻译成中文,那就是:一个类或者模块只负责完成一个职责(或者功能)。

注意,这个原则描述的对象包含两个,一个是类(class),一个是模块(module)。关于这两个概念,有两种理解方式。一种理解是:把模块看作比类更加抽象的概念,类也可以看作模块。另一种理解是:把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。

不管哪种理解方式,单一职责原则在应用到这两个描述对象的时候,道理都是相通的。为了方便你理解,接下来我只从“类”设计的角度,来讲解如何应用这个设计原则。对于“模块”来说,你可以自行引申。

单一职责原则的定义描述非常简单,也不难理解。一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。

我举一个例子来解释一下。比如,一个类里既包含订单的一些操作,又包含用户的一些操作。而订单和用户是两个独立的业务领域模型,我们将两个不相干的功能放到同一个类中,那就违反了单一职责原则。为了满足单一职责原则,我们需要将这个类拆分成两个粒度更细、功能更加单一的两个类:订单类和用户类。

3. 如何判断类的职责是否足够单一?

从刚刚这个例子来看,单一职责原则看似不难应用。那是因为我举的这个例子比较极端,一眼就能看出订单和用户毫不相干。但大部分情况下,类里的方法是归为同一类功能,还是归为不相关的两类功能,并不是那么容易判定的。在真实的软件开发中,对于一个类是否职责单一的判定,是很难拿捏的。

我举一个更加贴近实际的例子来给你解释一下。在一个社交产品中,我们用下面的 UserInfo 类来记录用户的信息。你觉得,UserInfo 类的设计是否满足单一职责原则呢?

public class UserInfo {private long userId;private String username;private String email;private String telephone;private long createTime;private long lastLoginTime;private String avatarUrl;private String provinceOfAddress; // 省private String cityOfAddress; // 市private String regionOfAddress; // 区private String detailedAddress; // 详细地址// ...省略其他属性和方法...
}

对于这个问题,有两种不同的观点。一种观点是,UserInfo 类包含的都是跟用户相关的信息,所有的属性和方法都隶属于用户这样一个业务模型,满足单一职责原则;另一种观点是,地址信息在 UserInfo 类中,所占的比重比较高,可以继续拆分成独立的 UserAddress 类,UserInfo 只保留除 Address 之外的其他信息,拆分之后的两个类的职责更加单一。

哪种观点更对呢?实际上,要从中做出选择,我们不能脱离具体的应用场景。如果在这个社交产品中,用户的地址信息跟其他信息一样,只是单纯地用来展示,那 UserInfo 现在的设计就是合理的。但是,如果这个社交产品发展得比较好,之后又在产品中添加了电商的模块,用户的地址信息还会用在电商物流中,那我们最好将地址信息从 UserInfo 中拆分出来,独立成用户物流信息(或者叫地址信息、收货信息等)。

我们再进一步延伸一下。如果做这个社交产品的公司发展得越来越好,公司内部又开发出了很多其他产品(可以理解为其他 App)。公司希望支持统一账号系统,也就是用户一个账号可以在公司内部的所有产品中登录。这个时候,我们就需要继续对 UserInfo 进行拆分,将跟身份认证相关的信息(比如,email、telephone 等)抽取成独立的类。

从刚刚这个例子,我们可以总结出,不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的。在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类。

除此之外,从不同的业务层面去看待同一个类的设计,对类是否职责单一,也会有不同的认识。比如,例子中的 UserInfo 类。如果我们从“用户”这个业务层面来看,UserInfo 包含的信息都属于用户,满足职责单一原则。如果我们从更加细分的“用户展示信息”“地址信息”“登录认证信息”等等这些更细粒度的业务层面来看,那 UserInfo 就应该继续拆分。

综上所述,评价一个类的职责是否足够单一,我们并没有一个非常明确的、可以量化的标准,可以说,这是件非常主观、仁者见仁智者见智的事情。实际上,在真正的软件开发中,我们也没必要过于未雨绸缪,过度设计。所以,我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构

听到这里,你可能会说,这个原则如此含糊不清、模棱两可,到底该如何拿捏才好啊?我这里还有一些小技巧,能够很好地帮你,从侧面上判定一个类的职责是否够单一。而且,我个人觉得,下面这几条判断原则,比起很主观地去思考类是否职责单一,要更有指导意义、更具有可执行性:

  • 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;

  • 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;

  • 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;

  • 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;

  • 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。

不过,你可能还会有这样的疑问:在上面的判定原则中,我提到类中的代码行数、函数或者属性过多,就有可能不满足单一职责原则。那多少行代码才算是行数过多呢?多少个函数、属性才称得上过多呢?实际上,也可以给你一个凑活能用、比较宽泛的、可量化的标准,那就是一个类的代码行数最好不能超过 200 行,函数个数及属性个数都最好不要超过 10 个。

实际上, 从另一个角度来看,当一个类的代码,读起来让你头大了,实现某个功能时不知道该用哪个函数了,想用哪个函数翻半天都找不到了,只用到一个小功能要引入整个类(类中包含很多无关此功能实现的函数)的时候,这就说明类的行数、函数、属性过多了。实际上,等你做多项目了,代码写多了,在开发中慢慢“品尝”,自然就知道什么是“放盐少许”了,这就是所谓的“专业第六感”。

4. 类的职责是否设计得越单一越好?

为了满足单一职责原则,是不是把类拆得越细就越好呢?答案是否定的。我们还是通过一个例子来解释一下。Serialization 类实现了一个简单协议的序列化和反序列功能,具体代码如下:

/*** Protocol format: identifier-string;{gson string}* For example: UEUEUE;{"a":"A","b":"B"}*/
public class Serialization {private static final String IDENTIFIER_STRING = "UEUEUE;";private Gson gson;public Serialization() {this.gson = new Gson();}public String serialize(Map<String, String> object) {StringBuilder textBuilder = new StringBuilder();textBuilder.append(IDENTIFIER_STRING);textBuilder.append(gson.toJson(object));return textBuilder.toString();}public Map<String, String> deserialize(String text) {if (!text.startsWith(IDENTIFIER_STRING)) {return Collections.emptyMap();}String gsonStr = text.substring(IDENTIFIER_STRING.length());return gson.fromJson(gsonStr, Map.class);}
}

如果我们想让类的职责更加单一,我们对 Serialization 类进一步拆分,拆分成一个只负责序列化工作的 Serializer 类和另一个只负责反序列化工作的 Deserializer 类。拆分后的具体代码如下所示:

public class Serializer {private static final String IDENTIFIER_STRING = "UEUEUE;";private Gson gson;public Serializer() {this.gson = new Gson();}public String serialize(Map<String, String> object) {StringBuilder textBuilder = new StringBuilder();textBuilder.append(IDENTIFIER_STRING);textBuilder.append(gson.toJson(object));return textBuilder.toString();}
}public class Deserializer {private static final String IDENTIFIER_STRING = "UEUEUE;";private Gson gson;public Deserializer() {this.gson = new Gson();}public Map<String, String> deserialize(String text) {if (!text.startsWith(IDENTIFIER_STRING)) {return Collections.emptyMap();}String gsonStr = text.substring(IDENTIFIER_STRING.length());return gson.fromJson(gsonStr, Map.class);}
}

虽然经过拆分之后,Serializer 类和 Deserializer 类的职责更加单一了,但也随之带来了新的问题。如果我们修改了协议的格式,数据标识从“UEUEUE”改为“DFDFDF”,或者序列化方式从 JSON 改为了 XML,那 Serializer 类和 Deserializer 类都需要做相应的修改,代码的内聚性显然没有原来 Serialization 高了。而且,如果我们仅仅对 Serializer 类做了协议修改,而忘记了修改 Deserializer 类的代码,那就会导致序列化、反序列化不匹配,程序运行出错,也就是说,拆分之后,代码的可维护性变差了。

实际上,不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、复用性、可维护性等。我们在考虑应用某一个设计原则是否合理的时候,也可以以此作为最终的考量标准。

经典设计原则:单一职责原则(SRP)相关推荐

  1. 设计原则(单一职责原则 开放封闭原则 里氏替换原则 依赖倒置原则 接口隔离原则 迪米特法则)

    设计原则 单一职责原则(SRP) 从三大特性角度看原则: 应用的设计模式: 开放封闭原则(OCP) 从三大特性角度看原则: 应用的设计模式: 里氏替换原则(LSP) 从三大特性角度看原则: 应用的设计 ...

  2. 六大设计原则-单一职责原则

    1.开闭原则        2.接口隔离原则 3.依赖倒置原则 4.迪米特原则             5.里氏替换原则     6.单一职责原则 单一职责原则 针对的问题 类T负责两个不同职责:职责 ...

  3. Java设计模式七大原则-单一职责原则

    目录 概述:设计模式的目的 设计模式七大原则 单一职责原则 单一职责原则注意事项和细节 概述:设计模式的目的 编写软件过程中,程序员面临着来自 耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性 等 ...

  4. 设计模式原则—单一职责原则

    设计模式概念与作用: 设计模式是一套被反复使用的.多数人知晓.经过分类编目的优秀代码设计经验的总结.特定环境下特定问题的处理方法. 1)重用设计和代码 重用设计比重用代码更有意义,自动带来代码重用 2 ...

  5. Single Responsibility Principle (SRP) - OO设计的单一职责原则

    前言 Robert C. Martin氏为我们总结了在面向对象的设计(OOD)中应该遵循的原则,这些原则被称为"Principles of OOD",关于"Princip ...

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

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

  7. 设计原则-单一职责原则

    单一职责原则 文章目录 单一职责原则 概述 单一职责原则问题由来 职责如何理解呢? 什么是职责呢 ? 举例1 Rectangle 个人理解 举例2 Modem SRP核心思想 总结 参考文档 概述 在 ...

  8. [置顶]       设计模式之六大原则——单一职责原则(SRP)

    定义: 应该有且仅有一个原因引起类的变更. There should never be more than one reason for a class to change. 优点: 1.类的复杂性降 ...

  9. 设计模式六大原则——单一职责原则(SRP)

    定义 就一个类而言,应该仅有一个引起它变化的原因.通俗的说,一个类只负责一项职责. 问题的由来 手机的功能多,但是每一项的功能都不强: 拍摄功能-->专业的摄像机和照相机 手机游戏-->P ...

  10. 设计原则 单一职责原则、开放封闭原则、依赖倒置原则、里氏代换原则、迪米特法则

    目录 1 单一职责原则 2 开放封闭原则 3 依赖倒置原则 4 里氏代换原则 5 迪米特法则 1 单一职责原则 比如:电脑内存坏了就应该更换内存,不应该更换CPU (内存负责内存.CPU负责CPU) ...

最新文章

  1. R语言之可视化-火山图
  2. Java 调用Oracle的存储过程
  3. js实现数据结构及算法之二叉树(Binary Tree)
  4. SAP系统上线后的变化
  5. 2021.02.05 Visual QA论文阅读
  6. 外挂学习之路(14)--- 游戏中的二叉树
  7. RabbitMQ系列(二)深入了解RabbitMQ工作原理及简单使用
  8. 机器视觉技术在表面缺陷检测方面的发展趋势
  9. 前端学习(2564):如何触发组件的更新
  10. Leetcode--210.课程表Ⅱ
  11. CloudIDE插件在手,按时下班不愁
  12. Unity下载文件一(www协程下载)
  13. C# 异步工具类 及一点小小的重构经验
  14. 离散数学左孝凌版答案
  15. java-jna win32 api使用
  16. STM32单片用什么编程?如何学习STM32单片机开发
  17. Quartz定时任务手动触发
  18. Unity 3D 热更新之基于 Asset Bundle Browser 的 AssetBundle包
  19. Treap(二)——#10144. 「一本通 4.6 练习 1」宠物收养所
  20. 32 | Linux系统和应用安全:如何大范围提高平台安全性?

热门文章

  1. Python保存图像的几种方式
  2. 开发SNS游戏时关于用户头像跨域
  3. 【数据库系统概论】——学习目录
  4. C语言实现的一个交互小程序(一)
  5. virtualbox虚拟机怎么识别U盘
  6. 运筹学计算机求解运输,运筹学上机报告运输问题的计算机求解.doc
  7. Symbian OS 初探
  8. 方正web开发平台之Web安全
  9. 端口扫描 -- K8PortScan
  10. 表格图片加载不出来,破图,加载失败