SOLID原则-单一职责原则

ops/2024/12/28 4:05:29/

转载请注明出处:https://blog.csdn.net/dmk877/article/details/143447010

作为一名资深程序员越来越感觉到基础知识的重要性,比如设计原则设计模式、算法等,这些知识的长期积累会让你突破瓶颈实现质的飞跃。鉴于此我决定写一系列与此相关的博客,希望对大家有帮助。提到设计原则可能大家或多或少都听过,单一职责原则、依赖倒置原则、接口隔离原则等,但是你是否对其有深刻的认识或者是否将其应用到真实的项目开发中呢?我先说说我作为一个10多年的程序员的真实感受,因为做Android应用开发在前几年可能更加注重Android基础FrameWork的学习,做了大概5年之后我拿到一个需求仍然花很少时间进行设计,基本都是直接就coding,虽然对设计原则和模式有点了解但是很少在开发需求的时候使用或者说就没有这个意识,慢慢的感觉到已经到了自己的瓶颈期,就是每天都在写代码,但是感觉没啥进步,自己的代码水平跟三年前差不多,于是开始寻求突破,突破的点就是从设计原则设计模式、算法、Android FrameWork层原理入手,经过几年的学习自己也明显感觉到自己写的代码越来越有层次也慢慢突破了自己的瓶颈,所以我准备写一系列设计原则设计模式相关的博客,一方面如果自己忘记了能够更快的回忆,一方面大家一起交流讨论从而加深对设计模式的了解。希望大家通过本篇博客的学习能够更加重视设计原则设计模式,如果你之前的习惯是拿到需求不做设计直接coding那么可以尝试改变一下思维,拿到需求后做下设计将设计原则和模式融入到代码中,这样随着时间的积累你肯定会有很大的进步。

废话不多说,进入正题,在众多的设计原则中比较经典的设计原则莫过于SOLIDSOLID设计原则是五个软件设计核心基本原则的首字母缩写,分别是:

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

  • 开闭原则(Open-Closed Principle, OCP)

  • 里氏替换原则(Liskov Substitution Principle, LSP)

  • 接口隔离原则(Interface Segregation Principle, ISP)

  • 依赖倒置原则(Dependency Inversion Principle, DIP)

SOLID是美国Robert C. Martin提出的,也是《架构整洁之道》的作者,我们今天主要介绍的就是SOLID中的单一职责原则

1 单一职责原则的定义

关于单一职责原则的定义,也是经过了一些演变,在《架构整洁之道》这本书提到,在历史上,我们曾经这样描述SRP这一原则

定义一:一个模块应该有且仅有一个被修改的原因

在现实生活中,软件系统为了满足用户和所有者的要求,必然要做出这样那样的修改。而该系统的用户或者所有者就是该设计原则中所指的"被修改的原因"。

如果一个或多个用户希望对系统进行的变更是相似的,就可以归为一类——一个或多个有共同需求的人。在这里,我们将其称为行为者(actor)。所以对SRP的最终描述就变成了:

定义二:一个模块应该对一类且仅对一类行为者负责。

原文:

Thus the final version of the SRP is:

​ A moudle should be responsible to one, and only one, actor.

这里的"模块"可以看做比类更加抽象的概念,类也可以看做模块。个人觉得两个定义差不太多,都可以用来理解单一职责原则,我们先来看几个生活中的例子

2 单一职责举例-软件公司的角色

在我们的软件开发中大致有这么几个角色,产品、UI、开发、测试,并且这些角色由不同的人承担,大家各司其职,为什么我们不一个人同时担任产品、UI、开发、测试呢?有几个方面的原因

(1)学习成本太高,一个人要同时学习产品、UI、开发、测试的知识,累成狗

(2)可替换性,如果一个人承担很多角色,突然一天他离职了,培训一个承担这么多角色的同事花费的代价太大了,相反如果各司其职,产品离职了招一个专门的产品就行,开发离职招一个专门的开发,花费的代价会小很多

其实在我们开发和架构上,也是这样的。从之前的单体架构,再到现在的微服务架构,我们会发现,服务的粒度越来越小,这样的主要好处就是,如果某个"微服务"出现了异常或者需要升级替换,对整个系统的影响也不会很大

3 单一职责举例-电脑配置

再举一个例子,假如你几年前买了一个台式机,现在你喜欢上了一个游戏,但是当前的电脑配置玩起来卡顿且画面质量差,怎么办呢?去重新买一台吗?当然不需要我们只需要买个大点的内存条+换一个性能好的显卡即可解决此问题,这就是单一职责的好处,电脑的各个配件负责不同的工作,想升级哪个配件只需要单独替换这一个配件即可,是不是非常方便?而且很好维护,如果电脑哪天坏了,维修人员检测到哪个配件坏了,直接替换即可,相反如果设计的时候把电脑设计成一个整体,它将很难做后期的维护,有可能一个小毛病导致整个电脑不可修复。因此单一职责使我们的产品可维护性更好、变更的风险更小,因此我们在设计接口时要尽量做到单一职责,设计颗粒度小、功能单一的类。

4 开发中的实例

接下来举个需求开发的例子,假如大学里要开发一个用户管理系统,我们将User类定义如下

public class User {private long userId;private String userName;private String nickname;private String email;private String phoneNumber;private String province; // 省private String city; // 市private String area; // 区private TeacherLevel teacherLevel; // 教师等级:讲师、副教授、教授private OfficeManager officeManager; // 办公管理者:比如网络管理、招生办管理、学籍管理等等...
}

我们先来分析下这个类,userId(用户Id)、userName(用户姓名)、nickName(昵称)这些属于用户最基本的信息,后面的email(邮箱)、phoneNumber(电话号码)、province(省)、city(市)、area(区)属于User的联系方式。

接着后面有个TeacherLevel表示教师的等级(讲师、副教授、教授等)

再往后还有个OfficeManager表示办公管理者类型(网络管理、招生办管理、学籍管理等)

我们的用户类型大致有三种分别为学生、教师、办公管理者。首先学生既不是老师也不是管理者,因此TeacherLevel、OfficeManager这两个字段对与学生来讲没有意义,同样对老师而言OfficeManager这个字段也没有意义,反之亦然。因此在这个类的设计里总有一些信息对一部分人是没有意义的,但这些信息对另一部分人又是必须的,而且这个类有以下几个问题:

  • User类庞大,在这个类包含着三种角色用户的信息
  • 任何需求方的改变都会导致User类修改,比如教师等级的修改、办公管理的修改

之所以会有这种情况就是因为类的职责不够单一,对比单一职责的定义,可以看到对于User这个"模块"它不仅仅对一类行为者负责,在单一职责的定义中我们提到"一个或多个共同需求的人我们称为行为者",在这里可以看到有三类行为者(学生、教师、办公管理者)。对于上述提到的第一种定义而言就是有多个原因导致User模块变化,而之所以有多个原因就是因为它有三种角色(学生、教师、办公管理者),那么我们是不是可以根据不同的角色进行拆分呢?来看下拆分后的结果

public class User {private long userId;private String name;private String nickname;private String email;private String phoneNumber;private String province; // 省private String city; // 市private String area; // 区.....
}
public class Teacher {private long userId;TeacherLevel teacherLevel;...
}
public class Office {private long userId;private OfficeManager officeManager;...
}

这里我们拆分出了Teacher和Office这两个类,把老师和办公人员相关的内容分别移到这两个类中,同时这两个类中都有一个userId,用来表示此角色跟哪个用户相关。这样是不是更清晰了,我们对Teacher和Office类的修改不会影响到User类。

随着业务的发展,我们开发的这个系统需要跟踪毕业之后的大学生的就业情况,比如他入职的公司、工作所在城市等,而且这样能更加方便大家的联系。那么大家毕之后会在全国各地找工作到时候所在的省、市、区是不是要经常改变,因此对于当前的User类我们还可以再拆分下

public class User {private long userId;private String name;private String nickname;private Contact contact;
}
public class Contact {private String phoneNumber;private String province; // 省private String city; // 市private String area; //区...
}

从刚刚这个例子,我们可以总结出,不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的。在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类。除此之外,从不同的业务层面去看待同一个类的设计,对类是否职责单一,也会有不同的认识。比如第一次拆分之后的User类中的信息都属于用户,满足单一职责。但是如果我们从更加细分的"用户联系方式"这个更细颗粒度的业务层面来看,User类还可以划分。在真实的软件开发中,类里的方法是归为同一类功能,还是归为不相关的两个类功能,并不是那么容易判定的。

​ 所以单一职责原则受很多因素的影响,纯理论来讲这个原则是非常优秀的,但是如何精确对类进行划分其实不简单,这需要我们对当前和未来系统的发展充分了解,即便如此随着项目的发展,可能有些类逐渐不满足单一职责,我们需要重新考量对其进行划分

5 类的职责是否越单一越好

答案是否定的,我们把单一职责原则推演至极致,一个类应该只有一个方法,这样它受的影响是最小的。但是在真实的项目开发中,一个类通常都不止一个方法,如果拆分的太细也有可能影响项目的可维护性等。

6 总结

(1)单一职责原则定义:一个模块应该对一类且仅对一类行为者负责。
(2)单一职责的好处:

  • 类的复杂性降低,实现什么职责都有清晰明确的定义
  • 可读性提高
  • 可维护性提高
  • 变更引起的风险降低

(3)单一职责原则一定要结合具体背景,伴随着项目的迭代

(4)单一职责原则并非职责越单一越好,单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

记住从今天开始将设计原则设计模式融入到你的需求开发中

如果大家有疑问或者发现错误,欢迎在下方留言,我会在第一时间回答!!
如果觉得文章对你有帮助就帮忙点个赞吧。

转载请注明出处:https://blog.csdn.net/dmk877/article/details/143447010

参考书籍:
《架构整洁之道》
设计模式之禅》


http://www.ppmy.cn/ops/130880.html

相关文章

SQL 视图:概念、应用与最佳实践

SQL 视图:概念、应用与最佳实践 SQL(Structured Query Language)视图是数据库管理中的一个重要概念,它允许用户以虚拟表的形式查看数据。视图在数据库中并不实际存储数据,而是提供了一个查询结果的快照,这…

庭田科技参与第四届计算机辅助焊接工程与增材制造国际研讨会

2024年10月18日,秋意盎然,魅力泉城济南迎来了一场科技与学术交融的盛宴——第四届计算机辅助焊接工程与增材制造国际研讨会(The 4th International Symposium on Computer-Aided Welding Engineering and Additive Manufacturing- CAWE-AM 2024)。此次盛…

Java基础语法①Java特点和环境安装

目录 1. Java的概念和用途 1.1 Java的概念和发展史 1.2 Java的重要性 1.3 Java的特点 2. Java环境 2.1 JVM 和 JDK 2.2 Java环境安装 2.3 安装IntelliJ IDEA并使用 写在前面:本人已经学习了C/C方向的内容,大二结束找到实习回学校后还有时间&…

blender雕刻基础 笔记

一、教学视频来源 案例5:荧光树桩_雕刻基础_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Bt4y1E7qn/?p18&share_sourcecopy_web&vd_sourced9dc363bbfe0ac72dbaa04823c59231e 二、笔记 1. 启动blender的雕刻模式 启动雕刻模式有两种方式&#x…

[CUDA] stream使用笔记

文章目录 1. stream一般用法2. stream与event:3. stream异常的排查4. stream的异步与同步行为 1. stream一般用法 cudaStream_t stream_; cudaStreamCreate(&stream_); // create stream // some operators running on this stream_ cudaStreamSynchronize(str…

如何引用一个已经定义过的全局变量?

如何引用一个已经定义过的全局变量? 答 、可以用引用头文件的方式,也可以用extern关键字,如果用引用头文件方式来引用某个在头文件中声明的全局变量,假定你将那个变量写错了,那么在编译期间会报错,如果你用…

Android -- (静态广播) APP 监听U盘挂载

Android – (静态广播) APP 监听U盘挂载 注册广播&#xff08;AndroidManifest.xml&#xff09;&#xff1a; <receiver android:name".receiver.MountReceiver"><intent-filter><action android:name"android.intent.action.MEDIA_MOUNTED"…

【基础】使用template替换yaml中的变量

前言 在接口自动化测试的时候&#xff0c;yaml 文件一般放测试的数据或当配置文件使用&#xff0c;yaml 文件存放静态的数据是没问题的&#xff0c;python的数据类型基本上都是支持的。 有时候我们想在 yaml 文件中引用变量来读取 python 代码的设置值。 template 使用 temp…