软件工程里面,有高内聚低耦合的概念。

那么,什么是内聚?什么是耦合呢?

内聚

所谓内聚,就是指一个功能模块内所有内容之间的关联度、相关性和紧密程度。

模块之内的方法、逻辑、语义以及其他成员之间,都有着很强的关联性,这就是高内聚。

一个好的程序设计必然追求高内聚,模块内的所有内容必须有着很强的关联性,它们才能被放在一个模块之内。

举个例子:设计一个用户模块,设计者一定是将用户相关的内容放在一个模块之内,不可能将毫无关联或者关联度极低的“新闻相关”内容放到用户模块里。

这就是一种对高内聚的追求,软件系统只有尽可能达到高内聚,才更有利于管理、维护、扩展。

耦合

所谓耦合,就是指不同功能模块之间的依赖关系。

比如,用户模块的所有功能,都不依赖于新闻模块,就算没有新闻模块,用户模块也能独立运行,这就是用户模块和新闻模块之间没有耦合。

但反之,如果用户模块依赖于新闻模块,那么,用户模块就没办法脱离新闻模块独立运行,当新闻模块有些东西发生改动的时候,说不定用户模块也需要改动才能正常运行,这就是耦合。

高内聚低耦合

软件开发上,从前期设计、规划,到后期开发实现,都应该秉承高内聚低耦合的思想。

只有尽可能地达到高内聚低耦合,程序才能更利于维护、拓展,从方方面面降低此程序的管理成本。

关于高内聚低耦合的思想,在充分理解内聚和耦合的概念后,再有一段时间的软件设计开发,回过头来想想,你就会发现这真的是真理。

这里要提一点,之所以要说“高”内聚“低”耦合,是因为现实中,通常不存在绝对内聚和彻底无耦合。

一个软件,本身就是由一个个功能模块组成,这些功能模块一起配合着完成软件的各种功能。

既然存在配合,那便必然存在耦合。

比如,你想要实现一个登陆注册功能,你会用到数据库模块、加密模块等。

这时候,你的登录注册模块必然依赖于数据库模块和加密模块。

所以,耦合在实际项目中是必不可少的。

另外,我们前面所说的“模块”,粒度也是不确定的。

比如,你可以把“模块”精确到一个类,那么你可能有登录模块、注册模块,你也可以把模块放大到分类,如前台、后台。

所以,高内聚低耦合的思想是从宏观的架构开始,一直贯彻到具体的代码文件的。理解这一点,非常重要。

我们常用的许多设计模式,编程范式,大多都是为了尽可能地实现“高内聚低耦合”。

我的思考

我认为:内聚过高,耦合必然升高,耦合降到最低,内聚也必然降低。

为什么这么说?

因为:想要内聚性达到最高,那就只在模块内放最单一的内容,没有其它内容与他关联,只有他自己,那关联度不用说也是最高的。

但是,这也导致了这个模块必然过分地依赖于外部模块,产生了极高的耦合。

因为内聚提高的同时,代表着功能变得越来越单一,越来越少,对外界的依赖就越大。

而耦合如果降到最低呢?

那各个模块之间如果彻底没有了互相依赖,那每个模块都能独立运行,每个模块都全能了。

那不用说,这个模块的内聚性肯定低的没话讲了。

比如,你登录模块完全不依赖外部模块,那你数据库查询也自己写,加密也自己写,但这些具体实现与登录基本没有太大的关联度。

所以,基于我的思考,我认为:高内聚低耦合思想的本身,就是带有平衡性质的。它追求在提高内聚的同时降低耦合,达到一种最好的平衡状态。

拔高视野

高内聚低耦合并非只是软件工程的思想,但凡是基数变大关系变复杂,高内聚低耦合都是解决混乱的好思想。

比如,团队管理时,只有三个人时,你可能甚至不需要有部门的概念。

但如果有三十个人,你就需要将这些人按关联度(内聚性)放在不同的部门中,而各个部门之间的依赖关系不能太复杂(低耦合)。

怎么理解呢?

比如,如果运营部日常工作,需要产品部、财务部、销售部、人力资源部、行政部、运输部……十几个部门的协助,那,任何一个部门缺席,都会导致运营部门不能干活!!

这还得了?

所以,在设计工作职责和机制、人员管理上,高内聚低耦合的思想依旧能提供一定指导。

精炼一点的话,只要复杂度和量级能够达到“工程”级,高内聚低耦合思想就都能提供指导。

说完内聚和耦合,接下来说几个很常见的词。

依赖倒置、控制反转、依赖注入,这三个词都是在面向对象设计中很常见的词语。

依赖倒置

依赖倒置是面向对象设计中能够降低耦合度的重要的设计原则。

它有两个主要思想:

  • 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象
  • 抽象不应该依赖于具体实现,具体实现应该依赖于抽象

怎么来理解呢?

我们需要先理解依赖这个词:当A依赖于B,我们可以理解为,A没有B就不行了,反过来B没有A无所谓。

在面向对象的设计中,我们会在开发具体逻辑之前,定义一些接口。

这个接口,就是抽象出来的东西。

接口本身没有任何能力,不提供任何具体的技能,他只是指导了具体功能应该长什么样子。(这就是抽象,抽象是非细节、非具体)

有了接口这层抽象,不论是调用方,还是实现方,都依赖于接口,而非依赖于具体,这就是一种依赖倒置。

比如,你出去买东西,你得拿钱去。

但在远古时期,大家都是以物易物的,那时候就很麻烦,但有了钱币这个抽象之后,大家依赖于钱币,不依赖于具体的物。这使得交易变得更加灵活。

那么,依赖倒置有什么好处呢?

试想,仍然以接口的抽象为例。

如果我们调用方直接依赖于实现方,那么实现方发生剧烈变动时,调用方的调用动作也得跟着变动。

当一个项目变得复杂时,一旦底层实现方发生一丁点变动,都有可能对上层数量庞大的调用方产生剧烈影响。

这也是高耦合的一种现象(上下层依赖关系太复杂)。

但如果,我们本着依赖倒置的原则,上下层都依赖于接口,那么,只要接口不变,上下层双方内部无论怎么变,都不会对对方产生影响。

所以,有时候接口,也会被称作契约。

因为只要你能履约,不论你发生什么改动,我都不管你。

依赖倒置原则,很好的降低了耦合度。

控制反转

控制反转,也是为了降低软件的耦合度而提出的一种面向对象设计原则。

怎么理解呢?

我们可以按照字面意义来理解,它就是在说:控制权被反转了。

比如,一件事情本来由A来控制的,现在变成了B来控制。

最形象的例子莫过于:new一个对象。

比如:

在A类中,我们需要依赖B类的实例,那我们会在A的代码中,new一个B,然后使用B的功能。

但我们会遇到一个问题,如果B哪天不能工作了,我们需要由C来替代B。

这时候,我们就需要在A的代码中修改new B那一段代码,改成new C。

如果,我们之前有A1、A2、A3……A100,都依赖于B,我们就需要分别从它们的代码中,把new B改成new C。

这显然没有什么维护性可言。

控制反转就是为了避免这种问题的发生而提出的设计原则(设计模式)。

就是让A1一直到A100,将new的控制权交出去。

他们只管得到一个实例,至于这个实例,谁来new,他们不管,他们只管这个实例能用就行。

那么,这些A就只负责取得实例以及使用其带来的功能,将控制权转出去的同时,也就不用去管B升级为C的破事了,谁控制谁去管吧。

此时,A就乐得清闲了。

那么,通过什么样的方式让A把控制权交出去呢?

或者说,如何实现控制反转呢?

依赖注入

依赖注入,是实现控制反转的一种方式。

从这句描述,我们就能知道依赖注入和控制反转之间的关系了。

这个具体的方式,其实很简单。

上面说把控制权交出去,那我们的A仍然还需要拿到一个B或者C的实例才能正常工作。

那就通过在A被实例化的时候,把这个实例从外界注入到A中去,常见的会在构造函数中注入。

这就是依赖注入。

其实就是通过你能想到的任何形式,将A想要的实例传到A里面。

注入这个词,很生动形象。

结合

那么,我们可以结合一下依赖倒置、控制反转、依赖注入这三个词。

实际上,我们从上面一句话中可以拆分出三个字各自的意义。

从这句话中,我们能够解读出,三者之间是互相配合的。

这句话是:

他们只管得到一个实例,至于这个实例,谁来new,他们不管,他们只管这个实例能用就行。

我们知道这句话如果实现了,那就是一次对控制反转原则的贯彻。

而上面“得到”一词,便是依赖注入来实现的。

“能用”一词,则是基于依赖倒置原则实现的。