“高内聚”与“低耦合”是软件设计和开发中经常出现的一对概念。它们既是做好设计的方法论,也是评价设计好坏的标准。

WHAT

“高内聚”是说,一个业务应当尽量把它所涉及的功能和代码放到一个模块中。

“低耦合”则是说,一个业务应当尽量减少对其它业务或功能模块的依赖。

图片

“高内聚低耦合”也是这么成双成对地出现

这一对概念其实不仅适用于软件行业。

在《写给大家看的设计书》中,作者就提出了“亲密性”原则。这一原则,其实就是“高内聚”的另一种阐述。

亲密性原则是指:内涵相关联的内容,在结构、关系上也应保持关联。

——写给大家看的设计书-读后笔记

在日常生活中,“高内聚”和“低耦合”也随处可见。

如果你是宅男/宅女,高内聚低耦合就是吃喝拉撒睡都不用出门,更不用跟人打交道。 图片

一只“高内聚低耦合”的喵……

如果你是上班族,高内聚低耦合就是你的工作不用找别人、别人的工作也不用找你,更不用和人沟通和扯皮。

如果你常跟政府部门打交道,一定还记得以往开一个证明要跑八九个部门、盖十几个章的麻烦,这其实就是政府服务"低内聚"的恶果。政府精简办事流程之后,一个窗口就能盖完所有章,这就是一种"高内聚"的服务。

图片

前几年这种漫画没少见吧

如果你是苹果的重度用户,一定对"封闭生态环境"体会深刻:它固然能提供很好的服务,但是深陷其中之后——iPhone、iPad、macbook、iCloud等——那么,即使其中某项服务无以为继了、或者满足不了需求了,也无法轻易地脱身而出、改换门庭。这就是"高耦合"带来的难题。

从上面的例子中不难看出,作为客户,我们通常都愿意享受高内聚低耦合的服务。但作为服务提供方,高内聚低耦合虽然有优点,也有一些不足。

WHY & WHY NOT

高内聚低耦合的优点和缺点都是显而易见的。

优点

如果一个模块、一个系统能够做到高内聚、低耦合,那么,它就具有了非常高的可扩展性。

可扩展性高意味着“未来有无限可能”:功能不满足新需求了,内部扩展就能满足;性能上达不到要求了,内部挖潜就能达标;技术太过陈旧了,内部迭代就能完成更新换代……与“外部协同”相比,“内部解决”有多高效,相信接触过的人都心里有数。

这么说可能有些费解,我们还是举例子吧。

对宅男/宅女来说,高内聚低耦合就是吃喝拉撒睡都不用出门,更不用跟人打交道。这样一来,自己在家想吃几块披萨就吃几块披萨,想喝几碗豆浆就喝几碗豆浆,完全不用顾虑别人想吃什么、别人会说什么。这样的生活惬意不?

对上班族来说,如果你是上班族,高内聚低耦合就是你的工作不用找别人、别人的工作也不用找你,更不用和人沟通和扯皮。这样一来,我想用想用Excel用Excel,想用Python就用Python;想写PPT就写PPT,想用Word汇报就用Word汇报。这样的工作快乐不? 图片

我反正是很快乐

技术上也是一样。一个高内聚低耦合的模块,内部想用ExecutorService就用ExecutorServcie,想用ForkJoinPool就用ForkJoinPool;想用线程池就用线程池,想用消息队列就用消息队列。这样的代码牛x不?

例如,我们系统中有一个模块,为用户提供短信验证功能。每次签约验证成功后,针对不同的业务,还需要做一些不同的后续处理。

后来,业务需求变更了五次,这里的“后续处理”也扩展了五次。除了业务需求之外,我们还做了一些技术优化,如优化库表结构、增加并发处理等。一通操作猛如虎之后,这个模块的流程变成了这样: 图片

短信验证模块的基本流程

虽然发生了如此翻天覆地的变化,这个模块的改造工作却推进得相当顺利。不仅模块自身的开发和测试简单而且轻松,而且其它模块对这个模块内部变化也都无感知、无影响。

尤其是每次业务需求变更后、增加新的后续业务处理时,已有后续业务处理从代码到数据结构都毫无牵涉。这就意味着开发不用修改原有老代码、测试不用回归测试原有功能,意味着上线周期可以缩短、生产环境不会引入新BUG……

这样多赢的局面,正是高内聚低耦合带来的第二个优点。

高内聚低耦合的设计能让我们只加新代码、不改老代码,从而减少要修改的代码量。对开发来说,改动的代码当然是越少越好。

高内聚低耦合的模块允许我们只测试新增功能、而不需要回归原有功能。对测试来说,测试范围当然是越小越好。

高内聚低耦合的设计能减少开发和测试的工作量,自然也就缩短了上线周期。对产品来说,需求上线的时间当然是越早越好。

高内聚低耦合的设计能降低模块变更的影响范围,从而减少出现BUG的几率;即使出现了BUG,也能保护原有功能不受新BUG波及。对用户来说,系统问题当然是越少越好。 图片

你好了不?

缺点

拥有如此“众口可调”的优点,为什么很少能在实际工作中听到或者应用“高内聚低耦合”呢?

首要的问题是成本。

如果你是一个政府部门,你是愿意自己只盖一个章、让用户去跑其它部门盖完剩下的十几个章,还是愿意让用户只来你这一个部门、你去跑其它部门改完所有的章?

要做到高内聚低耦合,通常要额外付出很多精力来做设计。符合高内聚低耦合要求的设计,在项目前期还常常带来会不少额外的开发工作量——高扩展性往往要到后期才能发挥作用。这些“额外”的工作量,就足以让很多人望而却步了。

以前面说的短信验证功能模块为例。为了满足高内聚低耦合的要求,我们足足多写了六个类(一个业务分发类、五个不同的业务处理类),前期开发也比计划多花了一天半天的时间。 图片

这是短信验证模块目前的类图

如果全都用if-else的方式,把所有逻辑都放在一个类中,确实会更加省时省力。但是,在业务逻辑本身比较复杂、或者彼此之间差异比较大、或者业务和系统有长期规划的情况下,还用if-else处理的话,代码极有可能会变成一团乱麻、成为“祖传代码”,并在后续维护中快速腐化。

可见,是否以高内聚低耦合为目标,实际上就是在前期成本和后期成本之间做博弈。追寻高内聚低耦合的足迹,几乎一定会推高前期成本,但后期成本不一定也能降低——这取决于前期设计和实现的质量;放弃高内聚低耦合这一目标,就一定能降低前期成本,也几乎一定会推高后期成本——优化重构也应当计入后期成本中。

选择\成本前期成本后期成本
遵循高内聚低耦合一定会推高可能会降低
放弃高内聚低耦合一定会降低一定会推高

选择高内聚低耦合之后是否能降低后期成本,是这个博弈中最大的不确定因素。等而次之的是这项业务、这套系统、这班人马能否坚持到项目后期。在这两个不确定因素面前,恐怕大多数人都会选择放弃高内聚低耦合,以较低的前期成本来完成项目吧。

其次,“高内聚低耦合”是一种原则性的设计准则。这一定位也是它落地实践的一个阻碍。

众所周知,原则性的东西一般都会为灵活性——比如工期啦,历史遗留问题啦,部门关系啦——让步。“下周就要上线了,先这样吧”、“以前就是这么做的,这次也这样处理吧”、“他们的接口就是这样的,我们也没办法”这样的话,经常成为放弃高内聚低耦合,乃至放弃优化设计、放弃深入思考的原因。有时真让人感叹,“技术驱动”、“工程师文化”未免太遥不可及了。

不仅如此,原则性的东西往往太高蹈凌空而无法落地施行。“这个模块不够高内聚”,“这段代码的耦合度太高了”,完全叫人丈二金刚摸不着头脑。具体是怎么不够高内聚?要改成怎样才能降低耦合度?讨论高内聚低耦合的文章里似乎都没有提过。

最后,太虚的方法无法具体施行,太虚的标准则无法让人“知止”。“高内聚低耦合”的度很难把握,无论怎样努力,都可以挑出“不够内聚”、“过于耦合”的毛病来。就像曾经的“正当防卫”一样,无论怎样小心谨慎,某些人总能挑出“不当”、“过当”之处来。这种走火入魔一般的行径,不仅令人诟病不已,于人于事也毫无裨益。这也是为什么“高内聚低耦合”很少在实际工作中提及和应用的原因之一。

图片

原则和实践……常常就是这么残酷

HOW

前面提到的这些问题中,有一些可以靠技术来解决。例如,开发成本高这个问题在团队开发水平提高到一定层次之后就迎刃而解了;太过务虚的问题也可以通过学习和掌握SOLID、设计模式等具体的设计技能来解决。这些具体的技术会在以后慢慢讨论。但是非技术的问题——例如历史包袱、部门关系等,我就爱莫能助了。

高内聚低耦合与抽象

虽然高内聚低耦合有上面这些问题,但是,在做业务抽象的设计时,我们还是要认真考虑、并遵守这个原则。因为高内聚低耦合不仅是做好设计的途径、也是评价设计好坏的标准:对于面向对象的业务抽象设计来说更是如此。

一个好的业务抽象必须能表达出自己“是什么”或者“能做什么”、而隐藏自己“怎么做”。高内聚、低耦合的理念正适合用来让业务抽象隐藏自己的实现细节。如果一个业务抽象不够内聚或者过度耦合,它一定会过多地把自己的实现细节泄露出去。

还记得在《抽象》一文中出现的QueryService吗?它把queryFromRemote和queryFromLocal暴露到业务抽象外部,导致了使用接口时的种种不便,这就是不够高内聚的结果。

public interface QueryService{
    public Bean queryFromRemote(long id);
    public Bean queryFromLocal(long id);
}

还有ExcelService,这个接口与2003版的Excel文件格式过度耦合了,结果无法顺利地升级到20007版Excel上。这就是不够低耦合带来的问题。

public interface ExcelService<T>{
    public List<T> read(HSSFWorkbook workBook);
}

总之,遵循高内聚低耦合,我们才能隐藏实现细节,才能完成高质量的抽象设计。

往期索引

《面向对象是什么》

从具体的语言和实现中抽离出来,面向对象思想究竟是什么?

《抽象》

抽象这个东西,说起来很抽象,其实很简单。