【架构设计】如何让你的应用做到高内聚、低耦合?

前言

最近review公司的代码,发现代码耦合程度特别高,修改一处,不知不觉就把其他地方影响到了,这就让我思考该如何让我们写的代码足够内聚,减少耦合呢?

"高内聚、松耦合"是一个非常重要的设计思想,能够有效地提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。它可以用来指导不同粒度代码的设计与开发,比如系统、模块、类,甚至是函数,也可以应用到不同的开发场景中,比如微服务、框架、组件、类库等。本文我们来探讨下如何让我们的应用做到高内聚、低耦合。

欢迎关注个人公众号『JAVA旭阳』交流沟通

什么是高内聚?

首先我们将目光投射到内聚上,通常我们的代码的内聚归为7类,如下图所示,内聚性从高到低。

  1. 功能内聚

将相同功能放到一个类或者模块中,内聚程度最高。

  1. 顺序内聚

如果一个功能的输出是另外一个功能的输入,这种存在顺序依赖关系的,我们将他们归到一个模块中叫做顺序内聚。

  1. 通信内聚

如果功能点使用相同输入或输出数据,我们将他们内聚到一个模块中,叫做通信内聚。

  1. 过程内聚

如果不同的功能是由同一个控制流支配的,我们称作过程内聚。

  1. 时间内聚

不同的功能在同一时间段内执行,比如银行不同的跑批任务,由时间去控制是否放在同一个模块中的内聚叫做时间内聚。

  1. 逻辑内聚

不同的功能可能他们内部的逻辑是一致的,我们将他归在一起叫做逻辑内聚。

  1. 偶然内聚

而偶然内聚是指不同的功能,没什么关联就直接放在一起了。这种情况很常见,比如A团队同时承担了支付、物流、产品等功能的开发,为了节省开发资源,他们就把不相干的功能放到一个模块中,那么这种偶然内聚会随着业务发展遇到各种问题。

上面讲解了内聚的可能7种分类,大家不用太较真,了解一下就行。

小结一下,我们所说的高内聚,其实是用来指导类或者模块本身的设计,简单来说,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。

什么是低耦合?

现在我们聚焦到耦合上,耦合实际上关注在类与类之间或者模块与模块之间依赖关系的设计,我们通常有下面7种类型的耦合关系。

  1. 非直接耦合

两个模块之间没有直接关系,模块独立性最强

  1. 数据耦合

两个模块之间用参数传递关联,模块之间影响最小的耦合关系。比如订单系统输入物流编号ID,物流系统返回你具体的物流信息。

  1. 标记耦合

两个模块依赖同样的一个数据结构,传递的是数据结构。

比如租房费用计算系统,需要计算水费和电费,如果你传用户信息这个数据结构,让水费和电费系统领取用户对象的用水量和用电量,这就是标记耦合。更好的做法应该只需要传必要的用水量和用电量,而不是传输整个用户对象。

  1. 控制耦合

两个模块之间传输控制信息,比如某个标志或者开关,调用模块需要知道被调用模块的内部逻辑,增加了相互依赖和理解的复杂度。

  1. 外部耦合

一组模块需要与外部环境关联,这组模块访问同一全局变量,外部耦合有时候必不可少,但应尽量减少此类模块数量。

  1. 公共耦合

一组模块均访问同一全局数据区,这种情况叫做公共耦合。比如有一个公共变量,不同模块都去修改它。

  1. 内容耦合

一个模块直接操作或修改另外一个模块的内部书,一个模块不通过正常入口访问另外一个模块,这就是内容耦合,是最糟糕的情况,需要避免。比如直接调用另外一个类的set方法,所以这就要求我们不要无脑的把类的任何属性都加上set方法。

上面大致分享了耦合的7种情况,你们项目属于哪种情况呢?

这边再总结一下, 所谓低耦合是说,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。

高内聚,低耦合有什么关系?

前面讲解了内聚和耦合的多种情况,那么高内聚、低耦合之间有什么关系呢?

上图中左边部分的代码设计中,类的粒度比较小,每个类的职责都比较单一。相近的功能都放到了一个类中,不相近的功能被分割到了多个类中。这样类更加独立,代码的内聚性更好。一个类的修改,只会影响到一个依赖类的代码改动。我们只需要测试这一个依赖类是否还能正常工作就行了。

上图中右边部分的代码设计中,类粒度比较大,低内聚,功能大而全,不相近的功能放到了一个类中。这就导致很多其他类都依赖这个类。当我们修改这个类的某一个功能代码的时候,会影响依赖它的多个类。我们需要测试这三个依赖类,是否还能正常工作。

所以说, “高内聚”有助于“松耦合”,同理,“低内聚”也会导致“紧耦合”

如何做到高内聚、低耦合呢?

关于如何做到模块或者类的高内聚、低耦合,有一个很经典的指导法则,那就是迪米特法则

迪米特法则是说不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。换更形象点的说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。

展开来讲,我们在做设计的时候特别关注下面的一些要点。

  1. 多用接口隐藏实现的细节, 接口是一个契约,相对稳定。
  2. 尽量避免不同模块或者类之间共享全局变量,万一一个地方需要修改,连带的其他模块也会影响
  3. 能用private的地方,坚决不用public, 不要给类的任何属性都加set方法,体现良好的封装性,暴露越少,影响也就越少。
  4. 合理使用设计模式,因为设计模式会很好的保证代码的可扩展性、封装性
  5. 注意分层设计和调用,比如在业务层直接用SQL语句操作数据库,而应该讲数据库操作封装到DAO层中,业务层调用DAO层
  6. 避免直接操作或者调用其他模块或类(内容耦合),也就是直接调用set方法,而是应该通过接口调用
  7. 尽量使用数据耦合,少用控制耦合。比如传输一个普通的数据变量是合适的。但传输一个控制命令给你,让你去执行分支a还是分支b还是分支c,这就不是很合适,所以尽量采用一些其他的方法来规避掉这些控制命令的发送。
  8. 模块的功能划分尽可能的单一, 遵循单一职责原则
  9. 模块只堆外暴露最小限度的接口,遵循接口隔离原则
  10. 模块之间的交互要尽量少,接口的设计要尽量简单,比如能传基本类类型就不要传输整个对象。

总结

“高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。

所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。所谓松耦合指的是,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。

所以我们在设计的时候,尽量要多动动脑子,想想这个功能是不是属于这个模块的呢?他们之间的交互合理吗?复杂吗?有没有更简单的方式呢?多问自己几个问题,你做出的设计会越来越优秀。

欢迎关注个人公众号『JAVA旭阳』交流沟通

posted @ 2023-01-06 13:50  JAVA旭阳  阅读(1088)  评论(0编辑  收藏  举报