# 12.3 内聚原则——不是一家人,不进一家门
耦合和内聚是检验模块设计是否合理的主要目标。
‘低耦合,高内聚’原则可以简单的描述为 内亲外疏。
耦合分类:(耦合度从低到高)
无耦合—>消息耦合->数据耦合->印记耦合->控制耦合->外部耦合->公共耦合->内容耦合
内聚分类:(内聚度从高到低)
功能内聚->信息内聚->顺序内聚->通信内聚->过程内聚->时间内聚->逻辑内聚->偶然内聚
模块化的目的是把一个复杂的系统分解为若干个相对独立、简单、具有特定功能的软件单元,以便分而治之。
耦合是不可能完全消除的,因为模块之间就是通过耦合来协作的。
要避免的是非本质的、不切当的或过于紧致的耦合。比如:
当一个模块有权对另一个模块的内部数据进行直接访问或修改时,
二者的关系就属于内容耦合(content coupling),
应该通过信息隐藏来防止。
再如,两个模块因为共享某个全局变量而发生公共耦合(common coupling),
这种关系也不值得提倡,
可以通过引入适当的接口来代替。
局部化原则
在编程中,一个良好的习惯是尽量把密切相关的软件元素放在一起,这便是局部化原则:
让代码的物理紧密度和逻辑紧密度保持一致。
OOP语言相比过程式语言的一个重要优势:
多了一种局部化的机制,能把关联的数据和运算粘合在一个类中,并在此基础上配备了访问控制机制。
AOP也一样:
把散落在各处的具有相同横切关注的代码集中在一个切面模块上。
更广泛地,任何关注点分离(SoC)的过程都伴随着局部化,因为分离的目的是为了新形式的聚合。散失为了更好的局部化。
局部化还体现在配置文件上,每个配置文件都聚集了一些相关的系统设置。
配置文件一个让人诟病之处是
配置信息与其关联的对象分属不同的文件,造成理解上的障碍和维护上的不便。
从这方面说,它违背了局部化原则。
属性导向式编程(Attribute-Oriented Programming,@OP)可以解决这个问题:
无论是Java中的annotation,还是C#中的attribute,都能将一些配置信息或元数据与相关联的软件元素如类、方法、域等同放一处。
但配置文件不可能被完全取代:
有些信息不适合放在代码中;
与多个软件元素同时相关的信息最好统一管理;
配置文件的内容不会污染源代码,还能在运行期间内重新设置等。
此例反映出实现局部化的困难所在:
从不同的逻辑层面来考察,便有不同的代码聚合方式,但最终只能取其一。
局部化原则还能导出DRY原则,从而实现了单点维护(single point of maintaince)
因为:重复代码的逻辑紧密度最高,按照局部化原则,相应的物理紧密度也该最高——还有什么能比融为一体的密度更高呢?
局部化原则不仅能得出高内聚原则,还能得出低耦合原则
既然代码的物理紧密度与逻辑紧密度是一致的,那么逻辑关联大的代码应该包含在同一个模块中,否则他们的物理距离太远;
逻辑关联度小的代码不应该包含在一个模块中,否则他们的物理距离太近。
于是每个模块内部的关联度较高,模块之间的关联度较低。
职责、关注点与变化点是三位一体的
模块的每一种职责既是一个关注的焦点,也是一个潜在的变化点。
功能内聚(functional)
如果一个模块包含的所有元素——包括指令、数据的定义、其他模块的调用等——都在为完成一个任务而工作,那么这种内聚被称为功能内聚(functional)
设计原则
一个类好比一个team,每个成员都应该具有同心协力的团队精神。
涉及表现形式的职责与设计数据或逻辑的职责最好不要混为一体,因为它们的变化方向几乎总是正交的。
这就是为什么MVC中的视图、三层结构中的表现层被单独隔离的原因。
反过来,如果两个职责总是联动的,改变其中一个很可能会影响到另一个,哪怕它们看是关联不大,也能名正言顺地聚合。
类是对现实世界中的对象或人类思维中的概念进行模拟和抽象的结果。
SRP给予的启示:每个类都是单一职责的抽象,也是单一变化的封装。
这表明:一个理想的类在其所在的抽象层次上,既是一个最小的可重用单元,也是一个最小的可维护单元。
职责被抽象故易于重用,变化被封装故易于维护,而职责与变化的单一性有决定了类的最小性。
这是最理想的情况。如果一些类违背了SRP,却因种种原因暂时不便修改接口。还可以用ISP——接口隔离原则来弥补。
P377 图12-12 根据ISP来改造的ShoppingCart类图)
ISP主张:不应强迫客户依赖那些他们不用的方法。
这增加了一些复杂性,但是如果接口的内聚度较低、稳定性较低、客户面较广,则进行接口隔离是非常必要的,即使付出一定的代价也在所不惜。
ISP还有一个诱人之处:它不仅能有效地减少接口变化带来的副作用,还能方便地提供各种专门的实现已适应客户的需要。
SRP与ISP在保证高内聚的同时,也为低耦合作出了贡献。
假若一个类或接口有两组关联性不强的方法,那么依赖不同组的两个客户很可能本无关联,却因依赖相同的类或接口而发生耦合。