设计模式学习笔记 - 规范与重构 - 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.如何给代码解耦?

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

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LTspice(14) Noise仿真

LTspice(14) Noise仿真 好久没有更新LTspice的教程了,大家想了没? 截止目前LTspice已经更新到24.0.9。界面发生了一些变化,但主要功能并不受影响,新的版本改了UI,找东西更加方便了,界面如下图1所示。 图1…

CSP初赛备考—汉字与运算

汉字 英文字符 英文字符的编码有两种:①ASCII标准码,7位(128个字符)②ASCII扩展吗,8位(256个字符) 中文字符 汉字分为两级:①一级汉字:3755个,按汉语拼音字母的次序排列。②二级汉字:3008个,按偏旁部首排列。 那么,怎么编码呢?要使用区位码和字形码等等。 区…

VMware虚拟机安装Ubuntu kylin22.04系统教程(附截图详细步骤)

一、版本信息 虚拟机产品:VMware Workstation 17 Pro 虚拟机版本:17.0.0 build-20800274 ISO映像文件:ubuntukylin-22.04-pro-amd64.iso 二、安装步骤 打开虚拟机,点击创建新的虚拟机: 选择自定义: 硬…

新雀优化算法NOA求解机器人栅格地图最短路径规划,可以自定义地图(提供MATLAB代码)

一、星雀优化算法 星雀优化算法(Nutcracker optimizer algorithm,NOA)由Mohamed Abdel-Basset等人于2023年提出,该算法模拟星雀的两种行为,即:在夏秋季节收集并储存食物,在春冬季节搜索食物的存储位置。CEC2005:星雀优化算法(Nut…

基于51单片机的定时器时钟设计[proteus仿真]

基于51单片机的定时器时钟设计[proteus仿真] 时钟设计检测系统这个题目算是课程设计和毕业设计中常见的题目了,本期是一个基于51单片机的定时器时钟设计 需要的源文件和程序的小伙伴可以关注公众号【阿目分享嵌入式】,赞赏任意文章 2¥&…

搭建nacos集群,并通过nginx实现负载均衡

nacos、eureka、consul、zookeeper等都是常用的微服务注册中心,这篇文章详细介绍一下在Ubuntu操作系统上搭建一个nacos的集群,以及通过nginx的反向代理功能实现nacos的负载均衡。 目录 一、安装nacos 1、安装nacos 2、修改nacos配置文件 3、创建naco…

[C/C++]string类常用接口介绍及模拟实现string类

一:Cstring类的由来 在C语言中,字符串是以\0结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用…

注意!!墙裂推荐几个好用的实用小工具!一定会用到的!

前言 在开发的世界里,面对各种挑战和问题时,拥有一套合适的工具箱至关重要。这不仅能提升我们的工作效率,还能让复杂的任务变得简单,甚至在解决棘手问题的同时,还能让我们的心情略微舒畅。众所周知,有用的…

2024年k8s最新版本使用教程

2024年k8s最新版本使用教程 3. YAML语言入门3.1 基本语法规则3.2 支持的数据结构3.3 其他语法 4 资源管理4.1 k8s资源查询4.2 资源操作命令4.3 资源操作方式4.3.1 命令行方式4.3.2 YAML文件方式 5 Namespace5.1 查看命名空间5.2 创建命名空间5.3 删除命名空间5.4 命名空间资源限…

ZigBee技术与实践教程(无线传感网技术第三天)

1.MAC层规范 在IEEE802系列标准中,OSI参考模型的数据链路层进一步划分为逻辑链路控制子层和介子访问子层两个子层。MAC子层使用物理层提供的服务实现设备之间的数据帧传输,而LLC在MAC 层的基础上,在设备之间提供面向连接和非连接的服务&…

软考高级:电子商务角色和类型概念和例题

作者:明明如月学长, CSDN 博客专家,大厂高级 Java 工程师,《性能优化方法论》作者、《解锁大厂思维:剖析《阿里巴巴Java开发手册》》、《再学经典:《Effective Java》独家解析》专栏作者。 热门文章推荐&am…

力扣图论篇

以下思路来自代码随想录以及官方题解。 文章目录 797.所有可能的路径200.岛屿数量130.被围绕的区域1020.飞地的数量 797.所有可能的路径 给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不…