设计模式学习笔记 - 规范与重构 - 5.如何通过封装、抽象、模块化、中间层解耦代码?

前言

《规范与重构 - 1.什么情况下要重构?重构什么?又该如何重构?》讲过,重构可以分为大规模高层重构(简称 “大型重构”)和小规模低层次重构(简称 “小型重构”)。大型重构是对系统、模块、代码结构、类之间关系等底层代码设计进行重构。

对于大型重构来说,最有效的解决手段就是 “解耦”。解耦的目的是实现代码高内聚、松耦合。关于解耦,今天准备分三个部分来给你讲解。

  • 解耦为何如此重要?
  • 如何判定代码是否需要解耦?
  • 如何给代码解耦?

解耦为何如此重要?

软件设计与开发最重要的工作之一就是应对复杂。

人处理复杂性的能力是有限的。过于复杂的代码往往在可读性、可维护上都不友好。

那如何在控制代码的复杂性呢?

手段有很多,我个人认为最关键的就是解耦,保证代码松耦合、高内聚。如果说重构是保证代码质量不至于腐化到无可救药的地步的有效手段,那么利用解耦的方法对代码重构,就是保证代码不至于复杂到无法控制的有效手段

关于 “高内聚、松耦合”,在《设计原则 - 8.迪米特法则(LOD)》中有介绍。

实际上 “高内聚、松耦合” 是一个比较通用的设计思想,不仅可以指导细粒度的类和类之间关系的设计,还能知道粗粒度的系统、架构、模块的设计。相对于编码规范,它能够在更高层次上提高代码的可读性和可维护性。

不管是阅读代码还是修改代码,“高内聚、松耦合” 的特性可以让我们聚焦在某一模块或类中,不需要了解太多其他模块或类的代码,降低了阅读和修改代码的难度。而且,因为依赖关系简单,耦合小,修改代码不至于牵一发而动全身,代码改动比较集中,引入 bug 的风险也就减少了很多。同时,“高内聚、松耦合” 的代码可测试性也更加好,容易 mock 或者很少需要 mock 外部依赖的模块或者类。

此外,代码 “高内聚、松耦合”,也就意味着代码结构清晰、分层和模块化合理、依赖关系简单、模块或类之间的耦合小,那代码整体的质量就不会差。即便某个类或模块设计的不怎么合理,代码质量不怎么高,影响的范围也是非常有限的。我们可以聚焦于某个类或模块,做相应的小型重构。相对于代码结构的调整,这种改动范围比较集中的小型重构的难度就容易多了。

如何判定代码是否需要解耦?

怎么判断代码的耦合程度呢?怎么判断代码是否符合 “高内聚、松耦合” 呢?如何判断系统是否需要解耦重构呢?

间接的衡量标准有很多,前面我们也讲到了一些,比如,看修改代码会不会牵一发而动全身。此外,还有一个直接的衡量办法,就是把模块与模块的、类与类的依赖关系画出来,根据依赖关系图的复杂性来判断是否需要解耦重构

如果依赖关系复杂、混乱,那从代码结构上来讲,可读性和可维护性肯定不是太好,那我们就需要考虑是否通过解耦的办法,让依赖关系变得清晰、简单。当然,这种判断比较主观的,我们可以把它作为一种参考和梳理依赖的手段,配合其他衡量标准一块来使用。

如何给代码解耦?

1. 封装与抽象

封装和抽象作为两个非常通用的设计思想,可以应用在很多设计场景中个,比如系统、模块、lib、组件、接口、类等等。封装和抽象可以有效地隐藏实现的复杂性,隔离实现的易变性,给依赖模块提供稳定且易用的抽象接口。

例如,Unix 的 open() 文件操作函数,用起来很简单,但是底层的实现是非常复杂的。我们通过将其封装成一个抽象的 open() 函数,能够有效控制代码复杂性的蔓延,将复杂性封装在局部代码中。此外,因为 open() 函数基于抽象而非具体的实现来定义,所以我们在改动 open() 的底层实现的时候,并不需要修改依赖它的上层代码,符合我们前面提到的 “高内聚、松耦合” 代码的评判标准。

Unix 的 open() 涉及权限控制、并发控制、物理存储等等。

2.中间层

引入中间层能简化模块和类之间的依赖关系

在这里插入图片描述
上图是引入中间层前后的依赖关系图。引入数据存储中间层之前,A、B、C 三个模块都要依赖内一级缓存、Redis 二级缓存、DB持久化存储。在引入数据存储中间层之后,A、B、C 三个模块只需要依赖一个数据存储中间层即可。引入中间层明显地简化了依赖关系,让代码结构更加清晰。

此外,在进行重构的时候,引入中间层可以起到过渡的作用,能够让开发和重构同步进行,不相互干扰

比如,某个接口设计的有问题,需要先修改它的定义,同时,所有调用这个接口的代码都要做相应的改动。如果新开发的代码也用到这个接口,那开发就跟重构冲突了。
为了让重构能小步快跑,可以分四个阶段来完成接口的修改:

  1. 引入一个中间层,包裹老的接口,提供新的接口定义。
  2. 新开发的代码依赖中间层提供的新接口。
  3. 将依赖老接口的代码改为调用新接口。
  4. 确保所哟的代码都调用新接口之后,删除老接口。

这样每个阶段的工作量都不会很大,都可以在很短的时间内完成。重构跟开发冲突的概率也变小了。

3.模块化

模块化是构件复杂系统常用的手段。

不仅在软件行业,在其他行业这个手段也非常有用。对于一个大型复杂系统来说,没有人能掌控所有的细节。之所以我们可以搭建出如此复杂的系统,并且能维护得了,最主要的原因就是将系统划分成各个独立的模块,让不同的人负责不同的模块,这样即便在不了解全部细节的情况下,管理者也能协同各个模块,让这个系统有效运转。

很多大型软件(比如 Windows)之所以能做到几百、上千人有条不紊地协作开发,也归功于模块化做的很好。不同的模块之间通过 API 来进行通信,每个模块之间的耦合很小,每个小的团队聚焦独立的高内聚模块来开发,最终像搭积木一样将各个模块组装起来,构建成一个超级复杂的系统。

聚焦于代码层面。合理的划分模块能有效地解耦代码,提高代码的可读性和可维护性。我们在开发代码时,一定要有模块化意识,将每个模块当做一个独立的 lib 来开发,只提供封装了内部实现细节的接口给其他模块使用,这样可以减少不同模块之间的耦合度

实际上,从刚刚的讲解中我们可以发现,模块化的思想无处不在,将 SOA、微服务、lib 库、系统内模块划分,甚至是类、函数的设计,都体现了模块化的思想。

模块化思想更加本质的东西就是分而治之

4.其他设计思想和原则

“高内聚、松耦合” 是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码范围改动。在前面章节的讲解中,我们多次提到过这个涉及思想。很多设计原则都是以实现代码的 “高内聚、松耦合” 为目的。我们来一块回顾下有哪些原则。

单一职责原则

内聚性和耦合性并非独立的。高内聚会让代码更加松耦合,而实现高内聚的重要指导原则就是单依职责原则。模块或者类的职责设计得单一,而不是大而全,那依赖它的类和它依赖的类就会比较少,代码耦合也就相应的降低了。

基于接口而非实现编程

基于接口而非实现编程,能通过接口这样一个中间件,隔离变化和具体的实现。这样做的好处是,在有依赖关系的两个模块或类之间,一个模块或者类的改动,不会影响到另一个模块或类。实际上,这就相当于将一种强依赖关系(强耦合)解耦为了弱依赖关系(弱耦合)。

依赖注入

跟基于接口而非实现编程思想类似,依赖注入也是将代码之间的强耦合变为弱耦合。尽管依赖注入无法将本应该有依赖关系的两个类,解耦为没有依赖关系,但可以让耦合关系没有那么紧密,容易做到插拔替换。

多用组合少用继承

我们知道,继承是一种强依赖关系,父类与子类高度耦合,且这种耦合关系非常脆弱,牵一发而动全身,父类的每一个改动都会影响所有的子类。相反,组合关系是一种若依赖关系,这种关系更加灵活,所以,对于继承结构比较复杂的代码,利用组合来替换继承,也是一种解耦的有效手段。

迪米特法则

迪米特法则讲的是,不该有直接依赖关系的类,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。从定义上,我们可以看出,这条原则的目的就是为了实现代码的松耦合。至于如何应用这条原则来解耦代码,你可以回头看一看《设计原则 - 8.迪米特法则(LOD)》。

除了上面降到的这些设计思想和原则之外,还要一些设计模式也是为了解耦依赖,比如观察者模式等,这一部分内容后面再讲解。

回顾

1.解耦为何如此重要?

过于复杂的代码往往在可读性、可维护性上都不友好。解耦保证代码松耦合、高内聚,是控制代码复杂度的有效手段。代码高内聚、松耦合,也意味着,代码结构清晰、分层模块化合理、依赖关系简单、模块或类之间的耦合小,那代码整体的质量不会差。

2.代码是否需要解耦?

间接的衡量标准有很多,比如,看修改代码是否牵一发而动全身。直接的衡量标准是把模块与模块、类与类之间的依赖关系画出来,根据依赖关系图的复杂性来判断是否要解耦重构。

3.如何给代码解耦?

给代码解耦的方式有:封装与抽象、中间层、模块化,以及一些其他设计思想与原则,比如:单一职责原则、基于接口而非实现编程、依赖注入、多用组合少用继承、迪米特法则等。当然,该有一些设计模式,比如观察者模式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/448452.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

3.6研究代码(2)

指的是微电网运行参数。 在MATLAB中,randi([0,1],1,48) 会生成一个包含1*48个0或1的随机整数数组。这意味着数组中的每个元素都将是0或1。 MATLAB帮助中心:均匀分布的伪随机整数 - MATLAB randi - MathWorks 中国https://ww2.mathworks.cn/help/matlab/r…

Java零基础入门到精通_Day 1

01 Java 语言发展史 Java语言是美国Sun公司(StanfordUniversity Network)在1995年推出的 计算机语言Java之父:詹姆斯高斯林(ames Gosling) 重要的版本过度: 2004年 Java 5.0 2014年 Java 8.0 2018年 9月 Java 11.0 (目前所使用的) 02 J…

llama factory学习笔记

模型 模型名模型大小默认模块TemplateBaichuan27B/13BW_packbaichuan2BLOOM560M/1.1B/1.7B/3B/7.1B/176Bquery_key_value-BLOOMZ560M/1.1B/1.7B/3B/7.1B/176Bquery_key_value-ChatGLM36Bquery_key_valuechatglm3DeepSeek (MoE)7B/16B/67Bq_proj,v_projdeepseekFalcon7B/40B/18…

#LT8711V适用于Type-C/DP1.2/EDP转VGA应用方案,分辨率高达1080P。

1. 概述 LT8711V是一款高性能 Type-C/DP1.2 转 VGA 转换器,设计用于将 USB Type-C 源或 DP1.2 源连接到 VGA 接收器。 该LT8711V集成了一个符合DP1.2标准的接收器和一个高速三通道视频DAC。此外,还包括两个用于 CC 通信的 CC 控制器,以实现 …

文件服务器

文件服务器 # 构建NFS远程共享存储## 一、NFS介绍shell 文件系统级别共享(是NAS存储) --------- 已经做好了格式化,可以直接用。 速度慢比如:nfs,sambaNFS NFS:Network File System 网络文件系统&#xf…

C++面向对象..

1.面向对象的常见知识点 类、 对象、 成员变量(属性)、成员函数(方法)、 封装、继承、多态 2.类 在C中可以通过struct、class定义一个类 struct和class的区别: struct的默认权限是public(在C语言中struct内部是不可以定义函数的) 而class的默认权限是private(该权…

Windows虚拟机的安装

Windows系统 总结知识点记忆级别: 1级:熟练记忆讲过的所有知识点(按照授课顺序,笔记顺序来提问)2级:灵活应用所学知识点(不按照顺序提问,面临陷阱提问)3级:应用所学知识解决实际问题4级:扩展应…

24 深度卷积神经网络 AlexNet【李沐动手学深度学习v2课程笔记】(备注:含AlexNet和LeNet对比)

目录 1. 深度学习机器学习的发展 1.1 核方法 1.2 几何学 1.3 特征工程 opencv 1.4 Hardware 2. AlexNet 3. 代码 1. 深度学习机器学习的发展 1.1 核方法 2001 Learning with Kernels 核方法 (机器学习) 特征提取、选择核函数来计算相似性、凸优…

陈景东:集中与分布式拾音与声信号处理 | 演讲嘉宾公布

一、声音与音乐技术专题论坛 声音与音乐技术专题论坛将于3月28日同期举办! 声音的应用领域广泛而深远,从场所识别到乐器音响质量评估,从机械故障检测到心肺疾病诊断,声音都发挥着重要作用。在互联网、大数据、人工智能的时代浪潮中…

【python】random库函数使用简要整理

前言 简要快速清晰整理random库 函数 函数作用random()返回0-1间的浮点小数randint(1,10)返回1到10间的整数uniform(1,10)返回1-10间的小数randrange(1,10,2)从1每隔2取一个数到10,在这些数中返回一个choice(列表)从列表中随机返回一个 shuffle(列表) 对列表内容…

高等数学常用公式

高等数学常用公式 文章目录 内容大纲 内容 大纲 感谢观看 期待关注 有问题的小伙伴请在下方留言,喜欢就点个赞吧

群晖NAS使用Docker安装WPS Office并结合内网穿透实现公网远程办公

文章目录 推荐1. 拉取WPS Office镜像2. 运行WPS Office镜像容器3. 本地访问WPS Office4. 群晖安装Cpolar5. 配置WPS Office远程地址6. 远程访问WPS Office小结 7. 固定公网地址 推荐 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默&#xff…

C语言中的UTF-8编码转换处理

C语言UTF-8编码的转换 1.C语言简介2.什么是UTF-8编码?2.1 UTF-8编码特点: 3.C语言中的UTF-8编码转换处理步骤1:获取UTF-8编码的字节流步骤2:解析UTF-8编码步骤3:Unicode码点转换为汉字 4.总结 1.C语言简介 C语言是一门…

怎么做加密文件二维码?分享文件更安全

怎么做一个加密文件二维码?在日常的工作和生活中,通过扫描二维码来查看或者下载文件的方式,被越来越多的人所使用,一方面是二维码的成本低,另一方面有利于提升便捷性和用户体验。 为了保证内容的隐私性和安全性&#…

数据库期末速成100分训练,附练手数据库原件及教程

本文提供下面数据库代码的数据库原件,下载后可使用 教程如下: 1.打开sql sever 2.找到数据库 3.右键数据库点击“附加”,然后点击“添加” 4.导入数据库原件,点击确定 ps:如果没有sqlsever 或者页面编辑器&#x…

uniapp引入jQuery

安装 npm install jquery --saveoryarn add jquery引入 import Vue from vue import jquery from "jquery"; Vue.prototype.$ jquery;<template><view>abc</view> </template><script>export default {data() {return {}}} </scr…

tcp服务器客户端通信(socket编程)

目录 1.编程流程 2.代码演示 2.1 服务器代码 2.2 客户端代码 3.注意 3.1 ping命令 3.2 netstat命令 3.3 为什么memset? 3.4 哪个会阻塞? 3.5 显示连接信息 1.概念 1.1 编程流程 1.2 connect与listen connect方法执行后&#xff0c;会进行三次握手&#xff0c;建立连…

【python 】----Pytest基础知识与进阶知识

定义 用于编写和执行Python测试全功能测试框架(工具),是一个第三方库 安装 pip insatll pytest 安装pytest --version 校验 pytest的组成构成 不写调用语句也可以执行函数内容 在用例运行语句里面: -s:指的是开启与终端的交互,如果没有-s(程序不会输入与打印),一条用…

Java高级编程—注解

文章目录 1.注解的概述2.常见的Annotation示例2.1 生成文档相关的注解2.2 在编译时进行格式检查的注解2.3 跟踪代码依赖性&#xff0c;实现替代配置文件功能的注解 3.自定义Annotation4.JDK中的元注解4.1 Retention4.2 Target4.3 Documented & Inherited 5. JDK8中注解的新…

Vue2里,利用原生js input的 type=“file“时,获取上传成功后的文件名及文件内容。下载文件到本地

功能场景:现在有个上传下载文件的功能,不需要调后端接口,因为需求是不需要将文件存到数据库里。如下图,是上传成功的场景: 这里限制上传accept类型为pem, 这里主要用到了input的change事件,如果没上传文件则提醒上传文件后再下载,下载功能主要是运用创建a元素,传入blo…