说完,视频聊天栏那头的面试官,匆匆下了线。只留下怔在座位上一脸懵逼的安酱。
这场面试下来,安酱其实自我感觉还不错。面试官看起来比较亲和,问的问题也很常规,都是些关于TCP/UDP的区别,线程进程通信方式这类的问题。
安酱对这种热门的面试题早已经滚瓜烂熟,手边摆着的程序员面试宝典也已经有些破烂不堪,显然是翻阅了许多遍。但是面试官最后一句话却让他有些心底发寒。
总的来说,面试官问的问题并不多,主要集中在计算机网络和操作系统基本原理。但最后要结束的时候,面试官话锋一转,没有继续在刚刚守护进程的问题上深入,而是突然问了一个关于编程思想的问题。
「你平时代码写得多不多?知道为什么要低耦合高内聚吗?」这个问题开放程度比较高,按道理来说并不难。
可是对于安酱来说,却有些不太好回答。因为他并不是纯正的CS科班出身,同时在学校期间也没有接触过什么大项目。所谓作为「负责人」的项目经历实际上都不过是拼拼凑凑而成的小软件或者小系统。
「emmm...我平时代码写的不是特别多。我感觉低耦合就是让代码之间的联系小一点吧,然后高聚合可能就是把类似的功能写在一个函数里...大概就这个样子。」安酱强行回答了一波,他感觉这种东西太难描述了,脑子里模模糊糊的。
「没了吗?那你在做项目的时候有没有涉及到模块化或者组件化这些东西呢?」面试官依然保持着他那从始至终的微笑。
「嗯可能是项目比较小,应该都没怎么接触过...」瞬间没了底气。
「没关系哈,那感谢你的时间,回去再了解下低耦合高内聚吧。」面试官笑着点了下头,退出了聊天室。
这其实也是我的一段真实面试经历。刚开始出去准备求职面试的时候,由于缺乏完整全面的项目经历,连代码的高内聚低耦合都说不太清楚。而如今开始参与大型的项目开发时,往往离不开对这六个字的思考和实践。
高内聚,低耦合。想必写过代码的人都听说过这条原则,但是真要把六个字解释清楚却并不简单。因为这六个字的思想太浓缩了,包含了设计模式的几大原则,体现了面对对象的开发思想,甚至能提供软件系统架构演化的方案和思路。
所以这回咱们就试着深入浅出的聊聊为啥咱们的代码要高内聚,低耦合。
1 内聚
首先还是概念性的东西。
内聚:从功能角度来度量模块内的联系。
耦合:各模块间相互联系紧密程度的一种度量,取决于模块间接口的复杂性、调用的方式及传递的信息。
简单来说,内聚就是指某一段程序所能做的事情的关联性。如果这段程序所完成的功能都类似,比方说都在完成文件操作,或者网络请求等,那么这一段代码就会被认为是内聚性高的。而如果这段程序只是各种操作的混合,一会在处理文件,一会又在刷新界面,这样的程序就会被认为是内聚性低的。
内聚性最实在的要求就是每个模块尽可能独立完成自己的功能,不依赖于模块外部的代码。这里的模块实际上只是程序的一个粒度划分,可以是一个函数,一个类,也可以是一个包,一个空间。一般而言,粒度越大的模块所要求的内举性就越高。
内聚性根据模块内部不同的联系方式,还可以分为好几种,包括逻辑内聚、时间内聚、过程内聚等。不同的内聚方式实际上就是看代码是以什么形式组织起来的。比方说逻辑内聚,就是依据判断条件的不同从而执行不同的逻辑;时间内聚就是按照程序执行的时间顺序来组织不同部分的代码。
所以代码的内聚性是为了保证每个模块的功能单一,从而能够充当最基本的组件被其它程序调用。这样的代码结构更利于维护和架构升级。
2 耦合
耦合是一个很常见的概念,在很多领域都会用到。而在一些老项目的开发重构中,经常需要对程序做一些解耦的工作。所谓解耦就是降低程序整体的耦合性,提高不同模块之间的内聚性。
耦合是一件非常容易而没有成本的事情。耦合意味着混乱,意味着关系错综复杂,牵一发而动全身。同样耦合根据模块间的联系方式的不同,也可以分为好几种。这里挑了几组耦合方式来尝试着理解一下。
数据耦合: 调用模块和被调用模块之间只传递简单的数据项参数。相当于高级语言中的值传递。
控制耦合: 模块之间传递的不是数据信息,而是控制信息例如标志、开关量等,一个模块控制了另一个模块的功能。
外部耦合: 一组模块都访问同一全局简单变量,而且不通过参数表传递该全局变量的信息。
公共耦合: 一组模块都访问同一个全局数据结构,则称之为公共耦合。公共数据环境可以是全局数据结构、共享的通信区、内存的公共覆盖区等。如果模块只是向公共数据环境输入数据,或是只从公共数据环境取出数据,这属于比较松散的公共耦合;如果模块既向公共数据环境输入数据又从公共数据环境取出数据,这属于较紧密的公共耦合。
对于耦合实际上是很好理解的。假如把每个模块比喻成一个齿轮,那么高耦合度的系统就像下图所示。每个齿轮之间相互卡扣,一个齿轮的运动会带动其它多个齿轮同时运转。同时当多个齿轮独立运动时,还会因为相互卡死而导致系统被锁住。
在具体的项目中,这样的情况并不少见。最简单的场景就是,当一个对象依赖于另一个对象的实现,那就相当于建立了一层耦合。当这样的情况增多,整个系统中的依赖关系就显得臃肿而复杂。多个对象之间相互依赖,甚至还会有多重依赖循环依赖的情况。这种情况在大型软件系统的迭代中是非常致命的。
3 解耦
既然如此,那么在对软件架构进行升级优化时,就不可避免的需要对系统进行解耦。解耦的方式有很多,可以根据不同语言的特性或者框架本身提供的方案进行。但这些方案本质上的目的都是一个,减少齿轮间的依赖。
减少依赖很容易,无非就是让齿轮之间的距离远一点,你碰不着我,我也摸不到你。这在实际的系统里可表现为每一个对象不直接使用别的对象里的资源,包括方法属性等。
齿轮分开是分开了,但是很明显这样的系统是无法运转的。那么该如何去建立各个齿轮之间的联系并传递动力呢?这里有一种比较通用的方式。
既然每个齿轮不能直接相联,那么给它们加个中转站不就行了。通过一个别的东西来对其它对不同齿轮之间的消息或者运动进行传递。这是不是就像是集中式的电话转接线路,所以说分布式也并不是啥情况都好用的。
通常来说,这样的中转站可以是作为一种接口的服务(moduleService),或者说是一种管理器(Manager)。通过这样的中介来进行不同模块之间资源的请求和返回。
一句话而言,这种方案就是通过一个第三方在对不同模块/组件间的对象进行解耦。所有对象的控制权交由这个第三方执行,通过中间人返回某个对象所需要的外部资源(包括对象、资源、常量数据)。
看到这是不是有种似曾相识的感觉。没错,这就是常说的IoC容器。
4 IoC 容器
先装个逼,丢两个个名词。
IoC容器:Inversion of Control,“控制反转”。
DI:Dependency Injection,即“依赖注入”。
IoC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。(知乎大佬的解释)
其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。
依赖注入是在IoC的基础更为具体的一种描述,明确描述了「被注入对象依赖IoC容器配置依赖对象」。听起来有些深奥,不过概念不重要。
所以,IOC简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。
低耦合高内聚是在编程中是极其重要的思想,从局部决定着系统整体的发展和演进。说实话我也没怎么摸清楚,只不过是由于最近刚入职两个月,老大让进行一个小范围的重构,所以写篇文章记录一下子。水平有限,keep moving...