如何让你的架构设计应用做到高内聚、低耦合?
前言
最近 review 公司的代码,发现代码耦合程度特别高,修改一处,不知不觉就把其他地方影响到了,这就让我思考该如何让我们写的代码足够内聚,减少耦合呢?
"高内聚、松耦合"是一个非常重要的设计思想,能够有效地提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。它可以用来指导不同粒度代码的设计与开发,比如系统、模块、类,甚至是函数,也可以应用到不同的开发场景中,比如微服务、框架、组件、类库等。本文我们来探讨下如何让我们的应用做到高内聚、低耦合。
什么是高内聚?
首先我们将目光投射到内聚上,通常我们的代码的内聚归为 7 类,如下图所示,内聚性从高到低。
编辑
功能内聚
将相同功能放到一个类或者模块中,内聚程度最高。
顺序内聚
如果一个功能的输出是另外一个功能的输入,这种存在顺序依赖关系的,我们将他们归到一个模块中叫做顺序内聚。
通信内聚
如果功能点使用相同输入或输出数据,我们将他们内聚到一个模块中,叫做通信内聚。
过程内聚
如果不同的功能是由同一个控制流支配的,我们称作过程内聚。
时间内聚
不同的功能在同一时间段内执行,比如银行不同的跑批任务,由时间去控制是否放在同一个模块中的内聚叫做时间内聚。
逻辑内聚
不同的功能可能他们内部的逻辑是一致的,我们将他归在一起叫做逻辑内聚。
偶然内聚
而偶然内聚是指不同的功能,没什么关联就直接放在一起了。这种情况很常见,比如 A 团队同时承担了支付、物流、产品等功能的开发,为了节省开发资源,他们就把不相干的功能放到一个模块中,那么这种偶然内聚会随着业务发展遇到各种问题。
上面讲解了内聚的可能 7 种分类,大家不用太较真,了解一下就行。
小结一下,我们所说的高内聚,其实是用来指导类或者模块本身的设计,简单来说,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。
什么是低耦合?
现在我们聚焦到耦合上,耦合实际上关注在类与类之间或者模块与模块之间依赖关系的设计,我们通常有下面 7 种类型的耦合关系。
编辑
非直接耦合
两个模块之间没有直接关系,模块独立性最强
数据耦合
两个模块之间用参数传递关联,模块之间影响最小的耦合关系。比如订单系统输入物流编号 ID,物流系统返回你具体的物流信息。
标记耦合
两个模块依赖同样的一个数据结构,传递的是数据结构。
比如租房费用计算系统,需要计算水费和电费,如果你传用户信息这个数据结构,让水费和电费系统领取用户对象的用水量和用电量,这就是标记耦合。更好的做法应该只需要传必要的用水量和用电量,而不是传输整个用户对象。
控制耦合
两个模块之间传输控制信息,比如某个标志或者开关,调用模块需要知道被调用模块的内部逻辑,增加了相互依赖和理解的复杂度。
外部耦合
一组模块需要与外部环境关联,这组模块访问同一全局变量,外部耦合有时候必不可少,但应尽量减少此类模块数量。
公共耦合
一组模块均访问同一全局数据区,这种情况叫做公共耦合。比如有一个公共变量,不同模块都去修改它。
内容耦合
一个模块直接操作或修改另外一个模块的内部书,一个模块不通过正常入口访问另外一个模块,这就是内容耦合,是最糟糕的情况,需要避免。比如直接调用另外一个类的 set 方法,所以这就要求我们不要无脑的把类的任何属性都加上 set 方法。
上面大致分享了耦合的 7 种情况,你们项目属于哪种情况呢?
这边再总结一下, 所谓低耦合是说,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动。
高内聚,低耦合有什么关系?
前面讲解了内聚和耦合的多种情况,那么高内聚、低耦合之间有什么关系呢?
编辑
上图中左边部分的代码设计中,类的粒度比较小,每个类的职责都比较单一。相近的功能都放到了一个类中,不相近的功能被分割到了多个类中。这样类更加独立,代码的内聚性更好。一个类的修改,只会影响到一个依赖类的代码改动。我们只需要测试这一个依赖类是否还能正常工作就行了。
上图中右边部分的代码设计中,类粒度比较大,低内聚,功能大而全,不相近的功能放到了一个类中。这就导致很多其他类都依赖这个类。当我们修改这个类的某一个功能代码的时候,会影响依赖它的多个类。我们需要测试这三个依赖类,是否还能正常工作。
所以说, “高内聚”有助于“松耦合”,同理,“低内聚”也会导致“紧耦合” 。
如何做到高内聚、低耦合呢?
关于如何做到模块或者类的高内聚、低耦合,有一个很经典的指导法则,那就是迪米特法则。
迪米特法则是说不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。换更形象点的说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。
展开来讲,我们在做设计的时候特别关注下面的一些要点。
多用接口隐藏实现的细节, 接口是一个契约,相对稳定。
尽量避免不同模块或者类之间共享全局变量,万一一个地方需要修改,连带的其他模块也会影响
能用 private 的地方,坚决不用 public, 不要给类的任何属性都加 set 方法,体现良好的封装性,暴露越少,影响也就越少。
合理使用设计模式,因为设计模式会很好的保证代码的可扩展性、封装性
注意分层设计和调用,比如在业务层直接用 SQL 语句操作数据库,而应该讲数据库操作封装到 DAO 层中,业务层调用 DAO 层
避免直接操作或者调用其他模块或类(内容耦合),也就是直接调用 set 方法,而是应该通过接口调用
尽量使用数据耦合,少用控制耦合。比如传输一个普通的数据变量是合适的。但传输一个控制命令给你,让你去执行分支 a 还是分支 b 还是分支 c,这就不是很合适,所以尽量采用一些其他的方法来规避掉这些控制命令的发送。
模块的功能划分尽可能的单一, 遵循单一职责原则
模块只堆外暴露最小限度的接口,遵循接口隔离原则
模块之间的交互要尽量少,接口的设计要尽量简单,比如能传基本类类型就不要传输整个对象。
总结
“高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。“高内聚”用来指导类本身的设计,“松耦合”用来指导类与类之间依赖关系的设计。
所谓高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。所谓松耦合指的是,在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。
所以我们在设计的时候,尽量要多动动脑子,想想这个功能是不是属于这个模块的呢?他们之间的交互合理吗?复杂吗?有没有更简单的方式呢?多问自己几个问题,你做出的设计会越来越优秀。
评论