降低模块间耦合

栏目: 后端 · 发布时间: 5年前

内容简介:提到耦合,必须先提依赖。依赖不可避免,而是尽可能地降低耦合。模块依赖指模块之间发生了关系,如模块A调用了模块B的接口,则模块A依赖了模块B。依赖的英语是Dependency。模块依赖是系统内不可避免的,复杂的系统是都是分而治之,软件架构活动中最重要的事就是如何正确把系统分解,并定义他们之间关系。存在关系就会存在依赖,依赖是系统分解的必然产物。如果一个系统内的模块他们不存在任何的关联,那他们应该是划分为不同的系统;任何的模块没有与其它的模块发生关联,那这个模块就应该不存在这个系统中。

提到耦合,必须先提依赖。依赖不可避免,而是尽可能地降低耦合。

依赖

模块依赖指模块之间发生了关系,如模块A调用了模块B的接口,则模块A依赖了模块B。依赖的英语是Dependency。

模块依赖是系统内不可避免的,复杂的系统是都是分而治之,软件架构活动中最重要的事就是如何正确把系统分解,并定义他们之间关系。存在关系就会存在依赖,依赖是系统分解的必然产物。如果一个系统内的模块他们不存在任何的关联,那他们应该是划分为不同的系统;任何的模块没有与其它的模块发生关联,那这个模块就应该不存在这个系统中。

模块依赖关系,按生命周期阶段可分为:

  • 开发态依赖 :如开发模块A时,需要依赖其它模块提供的接口,数据结构等文件依赖;还有一种如测试依赖,仅仅发生在开发阶段,在测试时,需要依赖测试数据,测试框架等,测试完成就不需要了。
  • 运行态依赖 :在系统运行时,模块A必须依赖其它模块提供能力才能完成某种完整的功能或服务,依赖的形态可能是本地或远程接口,集中配置数据,模型数据信息等。

开发态依赖可能引发运行态依赖,但运行态依赖不一定需要在开发态就依赖。我们经常关注是最终的运行态依赖导致的问题,目前的微服务架构设计,减少了开发态的依赖,把依赖导致的问题后移到运行态。

模块之间最好还是单向的依赖,如果出现A依赖B,B也依赖A,那么要么是A、B应该属于一个模块,要么就是系统整体拆分有问题。一个完整的软件系统的模块依赖应该是一张有向无环图。

耦合

模块耦合是指去修改一个模块A,需要同时要求依赖它的模块也跟着修改,则他们发生耦合。耦合相比依赖强调的是变化及影响,耦合的英语是Coupling。

由于依赖关系必然存在,变化也不可能避免,当变化发生在某个模块时,影响可能会波开到其它模块,这是依赖带的危害:

  • 过多依赖 :如一个模块的变化,导致其它多个模块需要跟着发生变化,这是一种强耦合,也势必对系统带来非常多的修改,造成系统的不稳定。
  • 依赖传递 :如一个模块的变化,导致其多层的下游依赖需要跟着都发生变化,这也是一种耦合,带的的影响往往对系统难评估、不可控。

提到耦合,不得不提一下正交性。正交性是从几何学中借鉴过来的,从软件开发的角度来看,就是一个方法,类,模块的改动不对另一个方法,类,模块造成影响,那么它们就是正交的。正交性设计是有助于简化复杂度,因为任何操作均无副作用,也就能降低模块间的耦合。常说“高内聚,低耦合”,我理解的低耦合,其实是降低变化所带来的影响程度,尽可能地较小影响,甚至不感知变化而无影响。那依赖关系中,被依赖的模块需要设计为;

  • 稳定 :模块的功能,接口,模型尽可能是不经常变化的。
  • 抽象 :模块进行了抽象,屏蔽了实现具体细节,依赖看不到变化。

另外一种思路则是想办法控制和消除不必要的耦合,首先是减少不必要的依赖:

  • 内聚 :控制好模块划分粒度,一个模块跟另一个模块没有功能重叠,一个模型只做好份内事。
  • 紧凑 :模块暴露的接口,数据越少越好,他们之间越正交,一个模块的变动对另一个模块的影响就最少。

方法

降低耦合是软件界经常谈论的话题,软件大师们已给我们总结一些原则、方法论,下面是我的一些收集与整理。

依赖倒置–面向接口

依赖倒置原则是 Robert Martin 大师在 《Reduce Coupling》书中提出,一句总结就是将依赖关系倒置为依赖抽象,而抽象是往往建立在接口之上。

使用 Java 的同学,感受最深的是就是interface,在JDK代码中存在大量的设计是基于接口抽象,比如IO操作就抽象出InputStream与OutpuStream接口,定义了统一的Read与Write行为。而实现这些接口可能是本地文件,也可能是网络连接。又如JDBC抽象出核心的Connection与Statement,定义了统一的连接与 SQL 语句操作方法,可以达到不同的实现对接不同的数据库系统的目的。

抽象带来好处就是,以不变应万变,它隐藏了实现的细节,有效地隔离了变化,从而很大程度地避免了因变化带来更大的波及范围,因为抽象与具体相互完全分离。

同样,降低模块之间的耦合,首先是要面向接口来设计模块。领域驱动设计(DDD)告诉我们的怎么去定义模块的接口:

  • 领域就是问题域,有边界,一个模块至少是在领域内,解决其问题
  • 建立领域模型来解决领域中的核心问题
  • 领域模型是抽象了领域内的核心概念,解决其核心问题
  • 核心概念无关技术实现细节,基于接口定义概念
  • 梳理领域内的核心概念之间的关系,形成接口的依赖关系

不同的层面的模块,接口形态也有多种,小到语言级的Interface/trait,大到与语言实现无关的RESTful与gPRC接口,他们本质还是DDD中所说的领域通用语言一种描述呈现。

控制反转–关注点分离

控制反转(IoC)是Spring发家秘籍,作为一个框架(Beans管理容器),它成功有效地消除了应用中不同类之间的显示依赖关系。一句总结就是不要让你来调用我,我来主动调用你。

我们的代码实现,大都是组装一个个对象,对象A调用对象B,一直调用下去来完成某种功能,这也是常见的面向过程编程,顺序地组装各类过程。对象A需要主动地创建与管理对象B的生命周期,这是一种正向控制。而反向控制则是由框架来帮忙创建及注入依赖对象,对象只是被动的接受依赖对象,依赖对象的获取被反转了。

反转给我带来启示是,主从地位的变化,把创建和查找依赖对象的控制权交给了框架,由框架进行注入组合对象,带来的好处就对象与对象之间是松散耦合,一是方便测试,二是利于功能复用,使得程序的整个体系结构变得非常灵活。

同样,降低模块之间的耦合,使用控制反转思想,把调用者与被调用者分开。调用者不关心谁是被调用者,只要知道存在一个具有某种特定接口,达到关注点分离:

  • 一个关注点就是一个特定的目标或概念,一个模块只有一个关注点,聚集才能高内聚
  • 分离的目的是保证模块之间没有功能上的重复,形成正交性
  • 被分离的功能通过依赖注入完成逻辑组装

当然,控制反转的前提还是依赖倒置,依赖的对象变成是一个抽象(接口),并不关心接口的实现者是谁。

事件驱动–观察与订阅

经常会碰到这种困境: 模块之间常有一对多的依赖关系,当被依赖模块的状态变化时,其他所有依赖模块都要发生改变。需要维护这种具有依赖关系的对象之间的一致性,又不希望为了维护这种一致性导致模块之间紧密耦合。

撇清关系是降低彼此耦合最为直接手段,以事件的弱引用去解决模块边界的耦合。当模块A需要执行模块B中的业务逻辑,相比于直接调用,我们可以发送一个事件出来。模块B通过一种机制能够接收到这个事件,当这类事件被触发时再去执行它的逻辑。

事件也是的一种抽象,独立于这两个模块之外,这样使得模块之间相互独立,事件在模块之间也实现共享。事件驱动可能存在一个共享内核(事件分发器,事件总线),模块只依赖于这个共享内核,而无需知道彼此的存在,也就实现了解耦合。

事件驱动还有另外一个好处,可能降低模块间的时序耦合:

  • 有些业务处理需要耗费相当长的执行时间,不想看到用户耗费时间去等待这些逻辑处理完成,则可以作为异步任务来执行。所要做的是触发一个事件,让Worker来调度执行。
  • 有些业务逻辑不需要关注是否在同一个上下文环境中。例如在CQRS框架,命令与查询分离,面向查询优化,查询数据来源是事件的接收与记录。

对于事件的处理,通常有两种方式,他们的区别如下:

  • 观察者模式 :采用监听器(Listener),通过监听器来监听事件的发生,依据事件做出相应的处理,每个监听器一般小巧,专注于响应特定事件的单个职能。观察者模式常常用于对象或模块之间的一对多依赖,通过事件通知方式来达到解耦合的目的。
  • 发布订阅模式 :采用订阅者(Subscriber),发布订阅模式需要存一个共享内核,订阅者向这个内核订阅不同的主题(Topic),事件可能被这个内核过滤、缓存,甚至修改了。它更适当异步处理,发布订阅模式是观察者模式一种跨模块(不同的进程)间通讯的延伸。

像Vert.X框架是目前比较受欢迎的基于事件驱动的异步微服务框架,它最主要是把HTTP处理变成事件驱动,核心还是来源于Netty,搞Java的同学不妨多看看Netty源码。

结语

依赖不可避免,但可以降低耦合。降低耦合首先尽可能地是单向依赖;被依赖的模块是稳定的,面向抽象(接口)编程;模块接口操作尽可能无副作用,满足正交性;模块实现上关注点分离,聚集才能高内聚;事件的弱引用一定程度能解决边界与时序耦合。

最重要一点,随着需求的增加变化,依赖与耦合并不是一成不变的,需要不断地去重构才能达到某种平衡,没有绝对松耦合,只是在特定场景下一定程度的松耦合。松耦合目的是降低变化给系统带的危害,切莫本末倒置。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

计算统计

计算统计

Geof H.Givens、Jennifer A.Hoeting / 王兆军、刘民千、邹长亮、杨建峰 / 人民邮电出版社 / 2009-09-01 / 59.00元

随着计算机的快速发展, 数理统计中许多涉及大计算量的有效方法也得到了广泛应用与迅猛发展, 可以说, 计算统计已是统计中一个很重要的研究方向. 本书既包含一些经典的统计计算方法, 如求解非线性方程组的牛顿方法、传统的随机模拟方法等, 又全面地介绍了近些年来发展起来的某些新方法, 如模拟退火算法、基因算法、EM算法、MCMC方法、Bootstrap方法等, 并通过某些实例, 对这些方法的应用进行......一起来看看 《计算统计》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换