CategoryResourceRepost/极客时间专栏/设计模式之美/设计原则与思想:设计原则/15 | 理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

182 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="15 | 理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0e/3d/0e34889ffa24a0b7f7e0c5862809a43d.mp3"></audio>
上几节课中我们介绍了面向对象相关的知识。从今天起我们开始学习一些经典的设计原则其中包括SOLID、KISS、YAGNI、DRY、LOD等。
这些设计原则,从字面上理解,都不难。你一看就感觉懂了,一看就感觉掌握了,但真的用到项目中的时候,你会发现,“看懂”和“会用”是两回事,而“用好”更是难上加难。从我之前的工作经历来看,很多同事因为对这些原则理解得不够透彻,导致在使用的时候过于教条主义,拿原则当真理,生搬硬套,适得其反。
所以,在接下来的讲解中,我不仅会讲解这些原则的定义,还会解释这些原则设计的初衷,能解决哪些问题,有哪些应用场景等,让你知其然知其所以然。在学习的时候,希望你能跟上我的思路,把握住重点,真正做到活学活用。
## 如何理解单一职责原则SRP
文章的开头我们提到了SOLID原则实际上SOLID原则并非单纯的1个原则而是由5个设计原则组成的它们分别是单一职责原则、开闭原则、里式替换原则、接口隔离原则和依赖反转原则依次对应SOLID中的S、O、L、I、D这5个英文字母。我们今天要学习的是SOLID原则中的第一个原则单一职责原则。
单一职责原则的英文是Single Responsibility Principle缩写为SRP。这个原则的英文描述是这样的A class or module should have a single responsibility。如果我们把它翻译成中文那就是一个类或者模块只负责完成一个职责或者功能
注意这个原则描述的对象包含两个一个是类class一个是模块module。关于这两个概念在专栏中有两种理解方式。一种理解是把模块看作比类更加抽象的概念类也可以看作模块。另一种理解是把模块看作比类更加粗粒度的代码块模块中包含多个类多个类组成一个模块。
不管哪种理解方式,单一职责原则在应用到这两个描述对象的时候,道理都是相通的。为了方便你理解,接下来我只从“类”设计的角度,来讲解如何应用这个设计原则。对于“模块”来说,你可以自行引申。
单一职责原则的定义描述非常简单,也不难理解。一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。
我举一个例子来解释一下。比如,一个类里既包含订单的一些操作,又包含用户的一些操作。而订单和用户是两个独立的业务领域模型,我们将两个不相干的功能放到同一个类中,那就违反了单一职责原则。为了满足单一职责原则,我们需要将这个类拆分成两个粒度更细、功能更加单一的两个类:订单类和用户类。
## 如何判断类的职责是否足够单一?
从刚刚这个例子来看,单一职责原则看似不难应用。那是因为我举的这个例子比较极端,一眼就能看出订单和用户毫不相干。但大部分情况下,类里的方法是归为同一类功能,还是归为不相关的两类功能,并不是那么容易判定的。在真实的软件开发中,对于一个类是否职责单一的判定,是很难拿捏的。我举一个更加贴近实际的例子来给你解释一下。
在一个社交产品中我们用下面的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个。
实际上, 从另一个角度来看,当一个类的代码,读起来让你头大了,实现某个功能时不知道该用哪个函数了,想用哪个函数翻半天都找不到了,只用到一个小功能要引入整个类(类中包含很多无关此功能实现的函数)的时候,这就说明类的行数、函数、属性过多了。实际上,等你做多项目了,代码写多了,在开发中慢慢“品尝”,自然就知道什么是“放盐少许”了,这就是所谓的“专业第六感”。
## 类的职责是否设计得越单一越好?
为了满足单一职责原则是不是把类拆得越细就越好呢答案是否定的。我们还是通过一个例子来解释一下。Serialization类实现了一个简单协议的序列化和反序列功能具体代码如下
```
/**
* Protocol format: identifier-string;{gson string}
* For example: UEUEUE;{&quot;a&quot;:&quot;A&quot;,&quot;b&quot;:&quot;B&quot;}
*/
public class Serialization {
private static final String IDENTIFIER_STRING = &quot;UEUEUE;&quot;;
private Gson gson;
public Serialization() {
this.gson = new Gson();
}
public String serialize(Map&lt;String, String&gt; object) {
StringBuilder textBuilder = new StringBuilder();
textBuilder.append(IDENTIFIER_STRING);
textBuilder.append(gson.toJson(object));
return textBuilder.toString();
}
public Map&lt;String, String&gt; 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 = &quot;UEUEUE;&quot;;
private Gson gson;
public Serializer() {
this.gson = new Gson();
}
public String serialize(Map&lt;String, String&gt; 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 = &quot;UEUEUE;&quot;;
private Gson gson;
public Deserializer() {
this.gson = new Gson();
}
public Map&lt;String, String&gt; 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类的代码那就会导致序列化、反序列化不匹配程序运行出错也就是说拆分之后代码的可维护性变差了。
实际上,不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、复用性、可维护性等。我们在考虑应用某一个设计原则是否合理的时候,也可以以此作为最终的考量标准。
## 重点回顾
今天的内容到此就讲完了。我们来一块总结回顾一下,你应该掌握的重点内容。
**1.如何理解单一职责原则SRP**
一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。
**2.如何判断类的职责是否足够单一?**
不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:
- 类中的代码行数、函数或者属性过多;
- 类依赖的其他类过多,或者依赖类的其他类过多;
- 私有方法过多;
- 比较难给类起一个合适的名字;
- 类中大量的方法都是集中操作类中的某几个属性。
**3.类的职责是否设计得越单一越好?**
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。
## 课堂讨论
今天课堂讨论的话题有两个:
1. 对于如何判断一个类是否职责单一,如何判断代码行数过多,你还有哪些其他的方法吗?
1. 单一职责原则,除了应用到类的设计上,还能延伸到哪些其他设计方面吗?
欢迎在留言区写下你的答案,和同学一起交流和分享。如果有收获,也欢迎你把这篇文章分享给你的朋友。