我理解的「高内聚低耦合」

作者:咖啡不加糖 发布时间: 2025-08-28 阅读量:38 评论数:0

先讲个反面教材。我们有个类,名字叫通用工具类,里面 87 个静态方法,被项目里 200 多个文件引用过。它里面有日期格式化、有金额计算、有正则校验、有一段调用外部接口的逻辑、还有两个方法直接查了数据库。

这个类是我见过的最标准的低内聚加高耦合组合,而且它是被一片好意造出来的——每个人都想着「这个方法可能别人也要用,放公共的地方吧」。

内聚:看改动的理由

教科书上讲内聚是「模块内部元素的关联程度」,这个定义太抽象了,我用不上。我实际用的判断标准是:这个模块被修改的理由,是不是只来自一个方向?

回到那个工具类。产品要改金额的舍入规则,要动它;运营要加一种日期展示格式,要动它;上游接口改了协议,还是要动它。三个完全不相干的角色都能让同一个文件产生变更,那它的内聚性就是差的。

这个标准的好处是可以量化。我们后来真的统计过:那个文件半年内被 23 个不同的需求改过,产生了 41 次提交,而这些提交的作者来自四个不同的小组。作为对比,一个内聚良好的领域服务类,同期只被 3 个需求改过。

还有一个更朴素的信号:你没法给它起一个具体的名字。叫「通用」「公共」「工具」「助手」的东西,基本都是内聚失败的产物。名字起不具体,是因为职责本来就不具体。

耦合:不只是看依赖数量

大多数人讲耦合会看依赖了几个类。这个指标有用但不够,因为它漏掉了最重要的一维:你知道对方多少事。

举个例子。方法 A 依赖一个上下文对象,只依赖一个类,看起来耦合很低。但这个上下文对象有三十个字段,A 内部读了其中七个,还判断了某两个字段的组合状态。这意味着 A 隐含地知道了那七个字段的语义和它们之间的约束——一旦上游改了任何一个字段的含义,A 就可能悄悄出错,而编译器不会告诉你。

相比之下,一个显式接收五个具体参数的方法,虽然参数多,但它知道的信息更少、更明确。参数收窄本身就是一种解耦手段,我现在改老代码经常做的一件事,就是把大对象参数拆成它实际用到的几个值。

我还会看依赖的方向。底层模块引用了上层模块的类型,这是一个明确的坏味道,说明抽象层次搞反了。

但拆过头也是病

这几年我看到的另一个极端更让我头疼:一个功能拆了六层,每层只做一件事——转发。请求进来,控制器调门面,门面调服务,服务调领域服务,领域服务调仓储,仓储调数据访问对象。中间四层每层都只有一行代码,就是把参数原样传下去。

加一个字段要改六个文件、六个数据结构、六次映射。定位问题要在六个文件之间跳。这不是低耦合,这是把耦合摊薄了但总量没变,还额外加了导航成本。

我的判断是:一层抽象存在的理由,必须是它真的做了某种转换或者决策。如果它只是原样传递,它就该被删掉。当年有人告诉我多加一层是为了将来好扩展,我信了很久,直到发现那个「将来」在四年里一次都没来。

我现在的实操

  • 新建一个类之前先想能不能给它起一个具体的、有业务含义的名字。起不出来就先别建。
  • 看到方法参数是一个大对象但只用了几个字段,就把它拆开。
  • 定期看一下哪些文件的提交作者最分散,那些通常就是内聚最差的地方。
  • 加一层之前问自己:这一层如果直接删掉、让上下打通,会损失什么?答不出来就别加。

那个 87 个方法的工具类,我们最后用了三个月慢慢拆掉了。方法是每次有人要改其中一个方法时,顺手把它挪到该去的地方。没搞过集中的大重构,就这么一点点挪空的。

评论