【我的《冒号课堂》学习笔记】设计原则(3)内聚原则

内聚原则

“高内聚,低耦合”原则是软件模块设计的通用原则。实际上,该原则最早出现在结构化设计(structured design)中,后被引入对象式设计。耦合和内聚是衡量软件设计质量的两个重要指标,是检验模块设计是否合理的主要标准。其中,耦合(coupling)反映模块之间的关联程度,内聚(cohesion)反映模块内部的关联强度。

以上是常见的耦合类型和内聚类型。模块化的目的是把一个复杂的系统分解成几个相对独立简单、具有特定功能的软件组件,以便分开管理。模块职责单一而明确,关系简单清晰就是遵循了低耦高聚原则,保证了软件的可细节性、可维护性、可重用性和可测试性。

耦合不可能完全被消除,因为模块之间是通过耦合来相互协作的。我们需要避免的是非本质的、不恰当的或过于紧致的耦合。比如一个模块有权对另一个模块内部数据进行直接访问和修改时,二者的关系就属于内容耦合(content coupling),应通过信息隐藏来防止。再比如,两个模块因共享某个全局变量而发生了公共耦合(common coupling),这种关系也不值得提倡,可以通过引入适当的接口来代替。如果发现两个模块的依赖关系过于紧密,以至于降低耦合也是徒劳,那么很可能二者有着本质的关联,不妨将它们合为一体,原先高耦合的缺点立马变成了高内聚的优点。

接下来该谈高内聚了,从软件设计中的一个基本原则谈起。在编程中,一个良好的习惯是尽量把密切相关的软件元素放在一起,这便是局部化原则(locality principle):让代码的物理紧密度和逻辑精密度保持一致。模块化原则就是局部化的一个特例而已。局部化原则不仅适用于模块单位,还适用于块(block)单位,甚至还适用于非语法单位——用逻辑空行或代码注释隔离的代码片段。局部化可以增加代码的可读性和一致性,结合语法机制还能减少代码改动所产生的影响范围。全局变量之所以不被提倡,关键就是它的辐射范围过广而难以控制,可将其限制为静态全局变量(static global variable)或是局部变量,或者利用各种语法容器使其局部化,比如命名空间(namespace)、包(package)、函数、类、块等。这同时也印证了各种内部类(inner class)或嵌入类(nested class)存在的价值。类似地,能作为实例成员(instance variable)的就不要作为类成员(class variable),能做为局部成员的就不要作为实例成员。

局部化是信息隐藏的前提。如果事先不把相关成员局限在一个拥有访问控制能力的软件单位中,信息隐藏是无从实现的。这也体现了OOP语言相比过程式语言的一个重要优势:多一种支持局部化的机制,能把关联的数据和运算粘合在同一个类中,并在此基础上配备了访问控制机制。不仅OOP用到了局部化,AOP也一样:它把散落在各处的具有相同横切关注的代码集中在一个切面模块上。更广泛的,任何关注点分离(SoC)的过程都伴随着局部化的过程,因为分离的目的是为了新形式的聚合。散是为了更好地聚。

局部化不仅体现在源代码上,还体现在配置文件(configuration file)上。每个配置文件都聚集了一些相关的系统设置,可以方便地统一管理和修改,克服了硬编码的缺陷。但是配置文件有一个为人诟病的缺陷:配置信息与其所关联的对象分属不同的文件,造成理解上的障碍和维护上的不便。从这方面说,它违背了局部化原则。属性导向式编程(Attribute-Oriented Programming或@OP)可以解决这个问题:无论是Java中的annotation,还是C#中的attribute,都能将一些配置信息或元数据(metadata)与相关联的软件元素如类、方法、域等同放一处。但配置文件也不可能被完全取代,原因是多方面的:有些信息是不宜放在源代码中的:与多个软件元素同时相关的信息最好统一管理:配置文件的内容不会污染源代码,还能在运行期间被重新设置等。此例也反映出实现局部化的困难所在:从不同逻辑层面来考察,便有不同的代码聚合方式,但最终只能从中择一。

局部化原则还能导出DRY(Don‘t repeat yourself)原则,从而实现了单点维护(single point of maintenance)。理由是:重复代码的逻辑紧密度最高,按照局部化原则,相应的物理紧密度也应是最高。

既然代码的物理紧密度与逻辑紧密度是一致的,那么逻辑关联度大的代码应该包含在同一个模块中,否则它们的物理距离太远;逻辑关联度小的代码不应包含在同一个模块中,否则它们的物理距离太近。于是每个模块内部的关联度较高,模块之间的关联度较低。故此,从局部化原则不仅得出高内聚原则,还能得出低耦合原则。

局部化原则过于普适,还有一个更具体的内聚原则——单一职责原则SRP(Single Responsibility Principle),简称SRP。该原则主张:一个类应当只有一个变更的理由。模块的每一种职责既是一个关注的焦点,也是一个潜在的变化点。因而,职责、关注点与变化点是三位一体的。如果一个模块包含的所有元素——包括指令、数据的定义、其他模块的调用等——都在为完成同一个任务而工作,那么这种内聚被称为功能内聚(functional cohesion),乃为最理想的一种内聚。一个理想的类在其所在的抽象层次上,既是一个最小的可重用单元,也是一个最小的可维护单元。

如果一个类违背了SRP,却因种种原因暂时不便修改接口,我们还可以使用接口隔离原则ISP(Interface Segregation Principle)来弥补。ISP主张:不应强迫客户依赖那些它们不用的方法。经过接口隔离,把一个总接口细分为几个更专门、内聚度更高的接口。这虽然会增加一定的复制性,是否值得取决于接口的关联程度、稳定程度、客户范围等因素。如果接口的内聚度较低、稳定性较低、客户面较广,则进行接口隔离是非常必要的,即使付出一定的代价也在所不惜。ISP还有一个诱人之处:它不仅能有效地减少接口变化带来的副作用,还能更方便地提供各种专门的实现以适应客户的需求。

总结:

原文地址:https://www.cnblogs.com/guihuo/p/5645581.html