设计原则-单一原则SRP

尽管这些设计原则在表面上看起来很容易理解,你可能觉得一看就能理解和掌握它
们。然而,当真正将这些原则应用到实际项目中时,你会发现“理解”和“会应用”是两
个不同层次的概念,而“有效地应用”则更具挑战性。根据我过去的工作经验,许多同
事对这些原则的理解并不够深入,以至于在实际应用时过于教条主义。他们将这些原
则视为绝对真理,僵化地套用,而非灵活地运用,从而产生了反作用。

单一职责原则(Single Responsibility Principle,简称SRP),它要求一个类或模块
应该只负责一个特定的功能。这有助于降低类之间的耦合度,提高代码的可读性和可
维护性。

但是对于这里所实现的单一的设计原则也是仁者见仁,智者见智的,对我我们在日常中的设计开发的时候,有些时候对于一个方法或者模块的开发,也是可以去进行相对的耦合的,这个时候当我们所使用的模块或者是方法所关联的模块并不多的时候,我们进行一定程度上的聚合是对我们开发相应的模块和功能是有好处的。

但是当我们地 功能的使用不断的增多之后,我们会发现把每一个模块中的相应的功能进行相关的提取是更加有益于我们之后进行修改和业务的相关的拓展的

上边的概念中提到了类(class)和模块(module),关于这两个概念,在后边的学
习中可以视作一样。
我们可以把模块看作比类更加抽象的概念,类也可以看作模块。或者把模块看作比类
更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。

单一职责原则的定义描述非常简单,也不难理解。一个类只负责完成一个职责或者功
能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲
就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单
一,应该将它拆分成多个功能更加单一、粒度更细的类。
让我们通过一个简单的例子来理解单一职责原则:
假设我们需要实现一个员工管理系统,处理员工的信息和工资计算。不遵循单一职责
原则的实现可能如下:

class Employee {
private String name;
private String position;
private double baseSalary;
public Employee(String name, String position, double baseSalary) {
this.name = name;
this.position = position;
this.baseSalary = baseSalary;
}
// Getter 和 Setter 方法
public double calculateSalary() {
// 计算员工工资的逻辑
return baseSalary * 1.2;
}
public void saveEmployee() {
// 保存员工信息到数据库的逻辑
}
}

在我们初次学习面向对象的时候,这个代码是在我们眼中感觉十分的正确的,而且这段代码的开发是十分的方便的,使用idea上的相关的生成器就可以去简化我们开发的相关的时间的

但是对于我们进行大型的框架开发的时候,显然这个设计的方法是不合理的

所以我们可以去使用

// Getter 和 Setter 方法
public double calculateSalary() {
// 计算员工工资的逻辑
return baseSalary * 1.2;
}
}
class EmployeeRepository {
public void saveEmployee(Employee employee) {
// 保存员工信息到数据库的逻辑
}
}

将下面的两个方法进行一个相关的分解这样当我们去使用注解的时候,也会变的十分的方便

在遵循单一职责原则的代码中,我们将员工信息的持久化操作从 Employee 类中抽
离出来,放到了一个新的 EmployeeRepository 类中。现在, Employee 类只负
责员工信息的管理和工资计算,而 EmployeeRepository 类负责员工信息的持久
化操作。这样,每个类都只关注一个特定的职责,更易于理解、维护和扩展。
遵循单一职责原则有助于提高代码的可读性、可维护性和可扩展性。请注意,这个原
则并不是绝对的,需要根据具体情况来判断是否需要拆分类和模块。过度拆分可能导
致过多的类和模块,反而增加系统的复杂度

高内聚、低耦合:自己的事情自己去干,能不麻烦别人就不要去麻烦别人的

2、如何判断类的职责是否足够单一
从刚刚这个例子来看,单一职责原则看似不难应用。但大部分情况下,类里的方法是
归为同一类功能,还是归为不相关的两类功能,并不是那么容易判定的。在真实的软
件开发中,对于一个类是否职责单一的判定,是很难拿捏的。我举一个更加贴近实际
的例子来给你解释一下。
在一个社交产品中,我们用下面的 UserInfo 类来记录用户的信息。你觉得,
UserInfo 类的设计是否满足单一职责原则呢?


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

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

//对于这个地方的方法所产生的相关的属性的时候,我们要去思考这个地方
//private String province; // 省
//private String cityOf; // 市
//private String region; // 区
//private String detailedAddress; // 详细地址
//这里我们也可以去将这个模块进行一个相应的拆分这样的话会方便于我们在后期的调用时候更加合理
/**
对于这个问题,有两种不同的观点。
一种观点是,UserInfo 类包含的都是跟用户相关的信息,所有的属性和方法都隶属
于用户这样一个业务模型,满足单一职责原则;
另一种观点是,地址信息在 UserInfo 类中,所占的比重比较高,可以继续拆分成独
立的 Address 类,UserInfo 只保留除 Address 之外的其他信息,拆分之后的两个类
的职责更加单一。
哪种观点更对呢?实际上,要从中做出选择,我们不能脱离具体的应用场景。如果在
这个社交产品中,用户的地址信息跟其他信息一样,只是单纯地用来展示,那
UserInfo 现在的设计就是合理的。但是,如果这个社交产品发展得比较好,之后又
在产品中添加了电商的模块,用户的地址信息还会用在电商物流中,那我们最好将地
址信息从 UserInfo 中拆分出来,独立成用户物流信息(或者叫地址信息、收货信息
等)。
*/

所以,我么还有记住一句话,脱离了业务谈设计是耍流氓,事实上脱离了业务谈什么
都是耍流氓,技术服务于业务这是亘古不变的道理。
我们再进一步延伸一下。如果做这个社交产品的公司发展得越来越好,公司内部又开
发出了跟多其他产品(可以理解为其他 App)。公司希望支持统一账号系统,也就
是用户一个账号可以在公司内部的所有产品中登录。这个时候,我们就需要继续对
UserInfo 进行拆分,将跟身份认证相关的信息(比如,email、telephone 等)抽取
成独立的类。

从刚刚这个例子,我们可以总结出,不同的应用场景、不同阶段的需求背景下,对同
一个类的职责是否单一的判定,可能都是不一样的。在某种应用场景或者当下的需求
背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未
来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类。
除此之外,从不同的业务层面去看待同一个类的设计,对类是否职责单一,也会有不
同的认识。比如,例子中的 UserInfo 类。如果我们从“用户”这个业务层面来看,
UserInfo 包含的信息都属于用户,满足职责单一原则。如果我们从更加细分的“用户
展示信息”“地址信息”“登录认证信息”等等这些更细粒度的业务层面来看,那 UserInfo
就应该继续拆分。
综上所述,评价一个类的职责是否足够单一,我们并没有一个非常明确的、可以量化
的标准,可以说,这是件非常主观、仁者见仁智者见智的事情。实际上,在真正的软
件开发中,我们也没必要过于未雨绸缪,过度设计。所以,我们可以先写一个粗粒度
的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越
多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所
谓的持续重构(后面的章节中我们会讲到)。
听到这里,你可能会说,这个原则如此含糊不清、模棱两可,到底该如何拿捏才好
啊?我这里还有一些小技巧,能够很好地帮你,从侧面上判定一个类的职责是否够单
一。而且,我个人觉得,下面这几条判断原则,比起很主观地去思考类是否职责单
一,要更有指导意义、更具有可执行性:
类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需
要考虑对类进行拆分;
类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计
思想,我们就需要考虑对类进行拆分;
私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public
方法,供更多的类使用,从而提高代码的复用性;
比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的
Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清
晰;
类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,
如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应
的方法拆分出来。
不过,你可能还会有这样的疑问:在上面的判定原则中,我提到类中的代码行数、函
数或者属性过多,就有可能不满足单一职责原则。那多少行代码才算是行数过多呢?
多少个函数、属性才称得上过多呢?
比较初级的工程师经常会问这类问题。实际上,这个问题并不好定量地回答,就像你
问大厨“放盐少许”中的“少许”是多少,大厨也很难告诉你一个特别具体的量值。

如果继续深究一下的话,你可能还会说,一些菜谱确实给出了,做某某菜需要放多少
克盐,放多少克油的具体量值啊。我想说的是,那是给家庭主妇用的,那不是给专业
的大厨看的。类比一下做饭,如果你是没有太多项目经验的编程初学者,实际上,我
也可以给你一个凑活能用、比较宽泛的、可量化的标准,那就是一个类的代码行数最
好不能超过 200 行,函数个数及属性个数都最好不要超过 10 个。
实际上, 从另一个角度来看,当一个类的代码,读起来让你头大了,实现某个功能
时不知道该用哪个函数了,想用哪个函数翻半天都找不到了,只用到一个小功能要引
入整个类(类中包含很多无关此功能实现的函数)的时候,这就说明类的行数、函
数、属性过多了。实际上,等你做多项目了,代码写多了,在开发中慢慢“品尝”,自
然就知道什么是“放盐少许”了,这就是所谓的“专业第六感”。

而且我们还可以去思考一个问题就是当我们对一个方法进行一个相关的命名的时候我们会发现如果我们对这个方法命名是十分的方便的时候,说明我们代码的耦合度是不高的,当我们发现我们对这个模块中的类和方法真的真难去给他起一个相关的名字的时候,说明我们这个就要去对代码进行一个重新的审视

3、类的职责是否设计得越单一越好
为了满足单一职责原则,是不是把类拆得越细就越好呢?答案是否定的。我们还是通
过一个例子来解释一下。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);
}
}

一:什么是反序列化
序列化:
序列化就是将 java对象 转化为字节序列的过程。
序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。 为什么要把Java对象序列化呢?因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
注意:序列化是为了在传递和保存对象时,为了保证对象的完整性和可传递性。将对象转为有序的字节流,以便在网上传输或者保存在本地文件中。

反序列化:
反序列化就是将 字节序列恢复为java对象的过程。
有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。

二、序列化和反序列化的应用
两个进程在远程通信时,可以发送多种数据,包括文本、图片、音频、视频等,这些数据都是以二进制序列的形式在网络上传输。
java是面向对象的开发方式,一切都是java对象,想要在网络中传输java对象,可以使用序列化和反序列化去实现,发送发需要将java对象转换为字节序列,然后在网络上传送,接收方收到字符序列后,会通过反序列化将字节序列恢复成java对象。

三、序列化和反序列化地实现
1.JDK类库提供的序列化API:
java.io.ObjectOutputStream
表示对象输出流,其中writeObject(Object obj)方法可以将给定参数的obj对象进行序列化,将转换的一连串的字节序列写到指定的目标输出流中。
java.io.ObjectInputStream
该类表示对象输入流,该类下的readObject(Object obj)方法会从源输入流中读取字节序列,并将它反序列化为一个java对象并返回

序列化要求:
实现序列化的类对象必须实现了Serializable类或Externalizable类才能被序列化,否则会抛出异常

四、序列化和反序列化,遵循以下方法:
方法一:若student类实现了serializable接口,则可以通过objectOutputstream和objectinputstream默认的序列化和反序列化方式,对非transient的实例变量进行序列化和反序列化。

方法二:若student类实现了serializable接口,并且定义了writeObject(objectOutputStream out)和readObject(objectinputStream in)方法
则可以直接调用student类的两种方法进行序列化和反序列化

方法三:若student类实现了Externalizable接口,则必须实现readExternal(Objectinput in)和writeExternal(Objectoutput out)方法进行序列化和反序列化。

//以上是对序列化这方面内容的一个相关的补充

正常来说,我们会把序列化和反序列化放置到以一起去形成一个相关的方法

如果我们想让类的职责更加单一,我们对 Serialization 类进一步拆分,拆分成一个
只负责序列化工作的 Serializer 类和另一个只负责反序列化工作的 Deserializer 类。
拆分后的具体代码如下所示:
if (!text.startsWith(IDENTIFIER_STRING)) {
return Collections.emptyMap();
}
String gsonStr = text.substring(IDENTIFIER_STRING.length());
return gson.fromJson(gsonStr, Map.class);
}
}

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 类的代码,那就会导致序列
化、反序列化不匹配,程序运行出错,也就是说,拆分之后,代码的可维护性变差
了。


实际上,不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可
扩展性、复用性、可维护性等。我们在考虑应用某一个设计原则是否合理的时候,也
可以以此作为最终的考量标准。
4、重点回顾
今天的内容到此就讲完了。我们来一块总结回顾一下,你应该掌握的重点内容。
1. 如何理解单一职责原则(SRP)?
一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单
一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读
性、可维护性。
2. 如何判断类的职责是否足够单一?
不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单
一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执
行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:
类中的代码行数、函数或者属性过多;
类依赖的其他类过多,或者依赖类的其他类过多;
私有方法过多;
比较难给类起一个合适的名字;
类中大量的方法都是集中操作类中的某几个属性。
3. 类的职责是否设计得越单一越好?
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类
的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的
耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得
其反,反倒会降低内聚性,也会影响代码的可维护性。
}

对于如何判断一个类是否职责单一,如何判断代码行数过多,你还有哪些其他的方法
吗?

在这个类中所形成的相关的方法之间的关联度上,两者之间的关系是十分的密切的时候,是可以去放置在这个同一个类中的,当我们,去根据业务之间的相关的关系和实现的时候,我们发现两个的关系是拆分起来进行相关的方法的实现是更加的方便的时候,我们就可以去对这个类中的方法去分解开来,还有前面所述的命名方法

除了检查代码行数,还有几种方法可以帮助判断一个类是否职责单一:

  1. 耦合度和内聚度:检查类的成员和方法是否高度相关。如果一个类的成员和方法处理多个不相关的职责,则可能需要重构。

  2. 变化原因:分析类的变化原因。如果一个类的改变是因为多个不同的原因,那么它可能承担了过多的责任。

  3. 方法数量:类中的方法过多可能是一个信号,表明它可能承担了过多的职责。尽管这不是绝对的,但通常类方法较多可能表明类的责任较多。

  4. 依赖关系:查看类的依赖关系。如果一个类依赖于许多其他类,这可能意味着它的责任过多,或者它正在承担多个职责。

  5. 关注点分离:确保类的功能和逻辑被合理分离。如果一个类的功能没有明确分界,或者处理多个不同的关注点,则可能需要重新设计。

  6. 类的职责描述:为类编写一个简洁的描述,检查这个描述是否集中在一个明确的职责上。如果描述涉及多个不同的功能,那么类可能不符合单一职责原则。

这些方法可以帮助深入了解类的设计,确保它们符合单一职责原则,从而提升系统的可维护性和可扩展性。


单一职责原则,除了应用到类的设计上,还能延伸到哪些其他设计方面吗?

单一职责原则(Single Responsibility Principle,SRP)是面向对象设计中的一个核心原则,它强调一个类应该只有一个责任,也就是说,一个类应该只有一个引起它变化的原因。这个原则不仅适用于类的设计,还可以扩展到其他设计方面。以下是一些单一职责原则应用的延伸领域:

  1. 模块设计

    • 在模块化设计中,单一职责原则要求每个模块应具有明确的功能和责任。每个模块应该只处理一个特定的功能或业务逻辑,这有助于减少模块之间的依赖,提高代码的可维护性和可重用性。
  2. 功能分离

    • 在设计系统或应用程序时,可以将不同的功能分离到不同的服务或组件中。例如,在微服务架构中,每个微服务应该只处理一个特定的业务能力或功能。这种分离有助于提高系统的灵活性和可扩展性。
  3. 接口设计

    • 在接口设计中,单一职责原则建议每个接口应该只包含相关的方法,避免接口过于庞大或复杂。通过设计小而明确的接口,可以使实现这些接口的类更加专注于特定的功能,并且更易于理解和维护。
  4. API设计

    • 在设计API时,单一职责原则可以帮助确保每个API端点只执行一个特定的操作。这有助于提高API的清晰度和一致性,使调用者能够更容易理解和使用API。
  5. 配置和设置

    • 在系统配置和设置中,可以应用单一职责原则来确保每个配置文件或设置项只负责一个方面的配置。这有助于简化配置管理,并减少因配置更改而导致的问题。
  6. 任务和职责分配

    • 在团队协作和项目管理中,单一职责原则可以指导任务和职责的分配。确保每个团队成员或小组只负责特定的任务或领域,有助于提高效率和减少冲突。
  7. 测试

    • 在测试设计中,单一职责原则建议每个测试用例或测试函数应测试一个特定的功能或行为。这样可以使测试更加集中、清晰,易于定位和修复问题。
  8. 文档

    • 在文档编写中,可以应用单一职责原则来确保每个文档或章节专注于一个特定的主题或功能。这有助于提高文档的可读性和易用性,使读者能够更容易找到所需的信息。

应用单一职责原则有助于提高系统的可维护性、可扩展性和清晰度,无论是在类设计还是其他设计方面。通过关注每个元素的单一责任,可以更好地管理复杂性和应对变化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值