软件设计不是CRUD(4):耦合度的强弱(上)

news2024/11/25 14:59:53

在讨论如何稳定系统内各模块的分层设计前, 本文先介绍一下目前判断各模块间耦合度强弱的度量方式。这些度量方式,在实际工作中读者应该都涉及过,只是可能没有去做详细的划分归类。

1、模块间耦合强度度量

模块间的耦合强度分为以下几种(耦合强度从高到底),耦合强度越高越应该重新进行模块间耦合方式的设计,付出的设计成本也相对越低,但效果却不一定很显著。但要对耦合强度已经较低的设计进行修改,要求的业务抽象能力和模块设计能力就越高,设计成本也越高,普遍来看效果也会越显著。
在这里插入图片描述
本节内容我们将通过理论讲解和代码示例演示相结合的方式,为各位读者详细介绍这些耦合强度在我们平时的工作中是如何体现的。先从模块间耦合度最高的耦合方式开始,讲解他们的代码效果和存在的问题。

另外需要说明的是,虽然前文介绍过解除循环依赖是模块解耦设计的关键,但不代表较高的模块耦合度一定存在循环依赖,仅代表是否更容易产生循环依赖

1.1、内容耦合/逻辑耦合

初看这种耦合描述,有的读者就会问,内容不就是数据吗?那么内容耦合和数据耦合不就是一回事吗?既然是一回事,为什么两种叫法却有不同的耦合强度呢?那就只可能有一种情况,就是这里所说的“内容”和“数据”不是一回事。实际上它们确实不是一回事,内容耦合主要是指模块内部的具体业务处理过程和模块内的数据结构模型。
在这里插入图片描述
也就是说,这种耦合方式下如果被调用的模块,其内部行为过程或者数据模型发生了变化,那么这种变化就会被传导到调用方。例如,直接通过调用模块的数据表(持久层)功能,进行数据操作。这样一来,如果被调用模块的数据表发生了变化(增加/减少/修改了某个字段、新增了一张关联的数据表),那么调用方的业务逻辑就不得不进行变化。

内容耦合一般会出现一个调用现象,就是调用方对所调用的模块,其调用位置并不是一个固定的层面,而是可能对被依赖模块的不同层面进行调用。如下图所示,就是一种常见的内容耦合。
在这里插入图片描述
这可能由于A业务模块并没有为调用者提供统一的调用层,也可能由于调用者并没有按照调用规范进行调用,导致B业务模块针对A业务模块的调用分散在A业务模块的多个设计结构层面。内容耦合是典型的需要避免的耦合方式,它的缺点包括:

  • 很容易产生循环依赖:由于调用分散,开发人员并不明确知道该如何调用模块,也不清楚模块的分层定位,基本上就是随心所欲(怎么方便怎么来)地进行功能的调用。在这个过程中模块和模块的循环依赖就很容易产生了。

  • 在被调用模块的内容发生变化的情况下,很难控制涟漪:特别是当调用者直接调用了A业务模块的具体逻辑实现时,例如A业务模块的数据库设计实现层,那么就意味着B业务模块依赖了A业务模块的具体实现进行自己业务逻辑的实现。当A业务模块的处理逻辑、处理逻辑发生变化时,B业务系统的逻辑就不得不做出调整。如果还有其它调用者又调用了B业务模块的具体实现,那么这种逻辑调整就不得不进行传递。

  • 无法适应变化,技术债务积累速度很快:技术债务会以惊人的速度被积累,是显而易见的事情。特别是在大型系统中进行这样的模块设计,并且不在研发规范上对开发人员进行限制。那么5个调用者对同一个模块进行调用时,可能就会有5中调用方式,而且这些调用方式的逻辑可能还是重复的或者冲突的。以下代码是一个典型的内容耦合/逻辑耦合的常见示例:

// 以下是调用方代码
// 为了简化理解,这里使用了一些spring的注解
@Component
public class Invoker {
  // 其他模块的数据持久层功能
  @Autowired
  private TargetRepository targetRepository;
  // 直接调用目标模块的数据持久层方法
  // 注意,这里的数据持久层并不是本模块的,而是其它模块的
  public void findSomething() {
    // 查询到其它模块的数据
    TargetEntity entity = targetRepository.findSomething();
    // ...... 再依据这个entity对象做后续的处理
  }
}

以上代码可以看到当前调用者,引入了目标模块数据持久层的一个方法,然后得到了一个目标业务模块的具体数据结构。那么一旦对方数据表发生了变化,必然导致调用者的处理过程修改。

1.2、公共耦合/外部耦合

看到这个耦合的描述,有的读者又会有疑问:难道我在多个模块中,都调用一种工具方法,就是公共耦合了?我们一定要说明清楚,本系列文章都在讨论一个核心内容,是好的应用系统应该怎么进行设计,或者这么说:要怎么将业务需求设计成层次分明、功能稳定、性能充沛的应用系统。所以我们讨论的问题,一定都绕不开针对业务的讨论。

也就是说这里的公共耦合并不是指多个模块共同调用了某个工具方法,而是指多个模块由于业务边界设计不清楚,所以只能将本来属于多个模块的某种业务过程联合在一起,形成一个底层模块。然后多个模块都将这个过程视为一个公共的业务过程进行调用。如下图所示:
在这里插入图片描述
读者实际上可以看到,这种公共耦合就是本系列文章中《软件设计不是CRUD(2):降低模块间耦合性——需求场景》提到的一种所谓解决两个(或多个)模块出现循环依赖的“办法”,显然这种“办法”并不是好的办法。还有一种和公共耦合和相似的耦合强度,叫做外部耦合。这两种耦合强度的区别在于,需不需要将复杂的业务结构暴露给调用者。如果需要暴露,就是一种公共耦合;如果不需要暴露,就是一种外部耦合。写两种模块间耦合强度的设计缺点是:

  • 业务边界不明确,且调用者必须知道模块内部的工作逻辑,才能确定在哪个模块去调用相应的业务功能。这主要是因为多个模块中都包含了相同业务领域的处理逻辑,所以调用者必须首先翻阅内部逻辑,确认要调用A业务对外提供的某个功能,是应该调用A业务模块本身,还是需要调用其他模块中的功能。另外,这样的设计方式,也很容易堆积技术债务。

  • 公共耦合/外部耦合的内部,很容易出现循环依赖,且这个模块内部的业务边界更难进行拆分。之所以会有这样的公共模块,很多时候是由于A模块和B模块(甚至更多的模块)存在循环依赖的情况。为了尽快简化A模块和B模块的依赖关系,设计人员在没有更好调整办法的情况下,又下沉创建了一个新的模块并将A模块和B模块存在循环依赖的功能部分移动到这个新模块中。这样产生的模块既没有真正解决问题,又没有明确业务边界,而且很难适应后续更多的业务调整诉求。

  • 这种公共耦合/外部耦合很难控制涟漪。这主要还是因为这种耦合强度并没有去除调用者对调用模块内部实现逻辑的关注。一旦调用模块的内部实现发生了变化,就会导致调用者的业务逻辑发生修改。

1.3、控制耦合

从控制耦合开始,调用者就不再需要详细关注模块内部的实现逻辑,只需要保证从被模块提供的一个专门的调用层(通常是设计人员所称的服务层)进行模块功能的调用即可。控制耦合本身并没有规定在进行模块功能调用时,传递的控制信息是一个对象还是一个简单的数据类型,控制耦合关注的核心要点是:

  • 调用者不再需要关注模块内部实现逻辑,但是需要根据被调用模块要求来传输数据结构,以便进行被调用模块内部逻辑过程的控制。这个控制数据可能是一个普通的数值,也可能是传递结构中某一个(或多个)属性的值。也就是说,如果定义调用时传递的数据结构,话语权是在被调用方。

  • 调用者通过一个规范的、专用的调用层进行模块功能的调用。这就意味着,模块间的业务边界需要尽可能清晰,否则很难形成这样的调用层。举个例子,在基于Java语言进行应用程序的设计的过程中,如果设计人员发现一个业务模块很难设计出一个只存在interface的调用层,那么最可能的原因是业务需求在转变成具体设计的过程中,并没有划分好业务边界。

  • 这里有一个特定情况需要说明,如果当前模块并不存在业务过程,而是单纯工具性质的处理过程(例如计算日期的功能、字符串拆分功能、中文字符判定功能等等),且这些处理过程并不涉及对其它模块的调用。那么这些对于这些工具性质模块的调用,其耦合强度都属于控制耦合或更弱的耦合。
    在这里插入图片描述

  • 控制耦合不能完全确保不存在模块间的循环依赖,但可以减少出现循环依赖的可能性。这主要是因为控制耦合的评判标准要求业务边界清晰,而除非你设计的应用系统足够小,否则清晰的业务边界就是能设计出专门的调用层的前提。

  • 其次,控制耦合也不能保证外部调用者完全不需要了解模块内部的工作过程,因为外部调用者至少需要知道模块内部有哪几种工作逻辑,这些工作逻辑分别被传入结构或者传入参数的什么属性、什么值所控制。也就是说一旦被调用模块的内部的逻辑分支发生了变化(例如增加了一种处理场景、减少了一种处理场景),那么调用方的逻辑也可能需要修改。

注意:如果模块存在一个专门的调用层,那也不能说明这个模块的耦合度已经降低到控制耦合。但是,一个模块如果没有提供专门的调用层,而任由其他模块无规则的调用,那么这个模块的耦合强度一定没有降低到控制耦合。另外,所谓的只存在interface定义的调用层,可不是专指service这样的接口定义,后文会进行详细介绍。

1.4、标记耦合

在模块间耦合强度已经降低至控制耦合的基础上,如果被调用的模块要求调用者传入一个具体的、符合规定的模型结构,才能完成模块功能的调用,那么这种耦合程度被称为标记耦合。标记耦合的特点是:

  • 标记耦合一定首先满足控制耦合的特点,也就是说标记耦合一定是在模块具备了一个统一的、专门的调用层的情况下来进行讨论、识别的。另外由于标记耦合只是在描述调用时传入的模型特点,所以标记耦合并不会避免控制耦合存在的问题,例如调用者仍然需要清楚模块功能中的逻辑分支,仍然需要准备在被调用的模块分支变化时,改变自己的逻辑过程。

  • 标记耦合在调用时传入的结构(例如Java中进行调用时传入User对象)是一个具体的结构而不是一个抽象的结构。如果传入的是一种抽象结构,例如传入的是一个被向上转型的对象那么就不认为是一种标记耦合。

以下是一种标记耦合的代码示例:

// 这里定义了一个具体的用户对象.
public class UserInfo {
  private String account;
  private Date createTime;
  private Integer age;
  
  // ......
  // 这里省略了读者都是到的get/set方法
}

// ======== 接着在专门的服务层定义调用接口UserService,并在UserService的实现中书写具体的业务
// 专门的用户信息服务接口,交给外部模块进行调用
public interface UserInfoService {
  /**
   * 创建用户信息
   * @param userInfo 具体的用户对象
   */
  public void create(UserInfo userInfo);
}

// ======== 以下是具体的业务实现,写在UserInfoService的实现中
@Service
public class UserInfoServiceImpl implements UserInfoService {
  public void create(UserInfo userInfo) {
    // 这里书写了具体的业务过程
  }
}

如果说控制耦合关注点在于是不是有统一的、专门的调用层,调用者是否需要关注模块内的全部逻辑细节。那么标记耦合的关注点就是调用时传入的数据结构是否和业务特点有强相关。但是无论是控制耦合还是标记耦合,都不能完全屏蔽调用者对被调用模块内部实现的关注,无非是关注程度不一样而已。

那么有没有一种模块间的耦合方式,可以让调用者不用关注被调用模块的任何工作细节,无论被调用的模块内部工作过程、逻辑分支如何变化,都可以控制涟漪效果。下面我们试着做一些深入探讨。

// ======== 接下文

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

小仙女必备,1分钟就能做出精美的电子相册

不知道大家有没有这样的困惑,手机里的照片太多,长久以来很多照片都容易被忘记。这个时候我们就可以将照片制作成电子相册,方便我们随时回味那些照片里的故事。如何制作呢? 制作电子相册只需要一个简单实用的制作工具就可以轻松完成…

linux硬盘挂载(linux 修改某个磁盘挂载到新目录\lvm扩容)

文章目录 一、什么是硬盘挂载二、linux 修改某个磁盘挂载到新目录三、Esxi下扩容硬盘1. 判断一个已有的文件系统是否使用了LVM(逻辑卷管理)2. 原本文件系统没有使用lvm,还可以lvm扩容吗?3. 原有文件系统使用lvm场景下扩容(lvm扩容)了解LVMEsxi LVM扩容步…

C++模板编程与泛型编程之函数模板

文章目录 函数模板(第一部分)定义函数模板使用函数模板样例 两阶段翻译 Two-Phase Translation模板的编译和链接问题 多模板参数引入额外模板参数作为返回值类型让编译器自己找出返回值类型将返回值声明为两个模板参数的公共类型样例 默认模板参数样例 重载函数模板模板函数特化…

偏序关系用分治优化建图:ARC165F

https://atcoder.jp/contests/arc165/tasks/arc165_f 首先可以建图&#xff0c;然后变成求字典序最小的的拓扑排序 然后发现这样复杂度会炸&#xff0c;观察连边的条件是什么&#xff1a; l i < l j l_i<l_j li​<lj​ r i < r j r_i<r_j ri​<rj​ 这是个…

麒麟-v10系统添加字体方法

先找到需要添加的字库文件&#xff0c;一般为TTF文件。 例如&#xff1a;方正粗黑宋简体.ttf 在 /usr/share/fonts 路径下创建一个chines 文件夹 。 * * * 注意以下所有操作涉及到的操作命令&#xff0c;均需ROOT操作。 mkdir /usr/share/fonts/chines 三&#xff0e;将需…

Single Image Haze Removal Using Dark Channel Prior(暗通道先验)

去雾算法都会依赖于很强的先验以及假设&#xff0c;并结合相应的物理模型&#xff0c;完成去雾过程。本文作者何凯明及其团队通过大量的无雾图像和有雾图像&#xff0c;归纳总结出无雾图像在其对应的暗通道图像上具有极低的强度值&#xff08;趋近于0&#xff09;&#xff0c;并…

虚拟机没有桥接模式--物理机WiFi不见了--注册表修复

我们知道虚拟机有三种模式&#xff1a; vmnet0 桥接模式&#xff1b;vmnet1 仅主机模式&#xff1b;vmnet8 NAT模式 我自己以前一直用的NAT模式&#xff0c;今天突然要用到桥接模式&#xff0c;发现无法切换... 我下面这个是后面弄好了的&#xff0c;最开始是没有显示桥接模式…

运放电压跟随器为什么要加电阻

这个是运放构成的电压跟随器&#xff0c;他的特点是输出电压等于输入电压&#xff0c;它常常用来对信号进行隔离&#xff0c;缓冲和提高带载能力。 有时候我们还会在电压跟随器上加这两个电阻&#xff0c;其中R1主要是起保护作用&#xff0c;Rf主要是为了消除偏置电流对输出电压…

stable-diffusion-webui安装Wav2Lip

常见错误 1.错误&#xff1a;Torch is not able to use GPU; add --skip-torch-cuda-test to COMMANDLINE_ARGS variable to disable this check 修改代码&#xff1a; launch_utils.py 删除三个地方&#xff1a;

LangChain+LLM实战---文本分块(Chunking)方法

RAG是一个考验技术的工作 基于大模型的企业应用中很大一部分需求就是RAG——检索增强生成。 这个流程依然无法描述RAG的复杂性 RAG涉及的内容其实广泛&#xff0c;包括Embedding、分词分块、检索召回&#xff08;相似度匹配&#xff09;、chat系统、ReAct和Prompt优化等&…

Optional——优雅判空

初始化 Optional提供了三个初始化方法&#xff1a; SpringBootTest public class OptionalTest {Testpublic void testOptional() {Optional.empty();Optional.ofNullable(null);Optional.of(null);} }empty返回一个空的Optional对象。 of遇到空会报错&#xff0c;但是使用Op…

Python | 安装、环境配置及包的安装

Python | 安装、环境配置及包的安装 一、前言二、python安装及编辑器配置2.1 python安装2.2 python调试2.3 python编辑器 | PyCharm2.3.1 PyCharm下载2.3.2 PyCharm安装2.3.3 PyCharm启动界面2.3.4 PyCharm初步设置2.3.5 PyCharm环境配置(含Python Interpreter配置)2.3.5.1 New…

2003-2022年飞机航线信息数据

2003-2022年飞机航线信息数据 时间&#xff1a;2003-2022年指标&#xff1a;起点城市、起点城市所属地级市、起点城市所属省份、起点机场、终点城市、终点城市所属地级市、终点城市所属省份、终点机场、航空公司、航班、机型、出发时间、到达时间、准点率、班次_周一、班次_周…

pip安装apex报错ERROR: Could not build wheels for cryptacular.......

问题&#xff1a;在训练模型的时候需要安装apex包&#xff0c;遂即使用以下命令 pip install apex但是报错了&#xff0c;报错信息如下&#xff1a; WARNING: Building wheel for cryptacular failed: [Errno 2] No such file or directory: C:\\Users\\XXX\\AppData\\Local\…

Corel VideoStudio 会声会影2024剪辑中间的视频怎么删 剪辑中音乐太长怎么办

我很喜欢视频剪辑软件Corel VideoStudio 会声会影2024&#xff0c;因为它使用起来很有趣。它很容易使用&#xff0c;但仍然给你很多功能和力量。视频剪辑软件Corel VideoStudio 会声会影2023让我与世界分享我的想法&#xff01;“这个产品的功能非常多&#xff0c;我几乎没有触…

解决找不到msvcp120.dll,无法继续执行代码的办法,msvcp120.dll丢失的解决办法

在使用电脑的过程中出现了“找不到msvcp120.dll,无法继续执行代码”&#xff0c;通常出现这种错误的原因是因为电脑中的msvcp120.dll文件丢失&#xff0c;但是文件丢失就会导致电脑出现软件不能打开的情况&#xff0c;也可能会导致电脑出现其他的问题&#xff0c;所以今天就给大…

【学习草稿】

【数据分析】 1、相关性分析 对变量之间相关关系的分析&#xff0c;即相关性分析。其中比较常用的是线性相关分析&#xff0c;用来衡量它的指标是线性相关系数&#xff0c;又叫皮尔逊相关系数&#xff0c;通常用r表示&#xff0c;取值范围是[-1,1]。 r的绝对值<0.3 ,低度线性…

spring报错 @EnableAsync annotation metadata was not injected

报错 报错 internalAsyncAnnotationProcessor 这个spring内部的后处理器 创建失败&#xff0c;进而导致 EnableAsync 注解元数据没有注入容器 分析问题 查了 博客 是配置类放到原始项目路径下导致的问题。 博主的路径虽然正确&#xff0c;但发现是相似的问题&#xff0c;最…

linux 驱动——将模块编译进内核

文章目录 新增 C 文件修改 Makefile 文件修改 Kconfig 文件模块使能内核启动日志参考 linux 驱动——字符设备驱动 linux 驱动——字符设备驱动(自动生成设备节点文件) linux 驱动——将模块编译进内核 前面两节介绍的驱动都是以模块的形式&#xff0c;需要手动加载&#xff0…

【kubernetes】pod的生命周期

文章目录 1、概述2、pod的生命期3、pod阶段4、容器状态5、容器重启策略6、pod状况6.1 Pod就绪态6.2 Pod就绪态的状态6.3 Pod网络就绪 7、容器探针7.1 检查机制7.2 探测结果7.3 探测类型 8、Pod的终止8.1 强制终止Pod8.2 Pod的垃圾收集 1、概述 pod遵循预定义的生命周期&#x…