面向对象技巧
内聚
当模块的元素全部专注于模块的职责的时候,即使元素间的结合不是很紧密,也符合内聚性的要求。简单的说,就是“不要挂羊头卖狗肉”。
- 巧合内聚
模块内部的元素被划分在一起,仅仅是因为“巧合”。
- 逻辑内聚
模块内部元素之所以被划分在一起,是因为这些元素逻辑上属于同一个比较宽泛的类别。
比如将鼠标、键盘划分入输入类,将打印机、显示器划分为输出类。
- 时序内聚
模块内部的元素之所以被划分在同一个模块中,是因为这些元素必须按照固定的“时间顺序”进行处理。
- 过程内聚
模块内部的元素之所以被划分在同一个模块中,是因为这些元素必须按照固定的“过程顺序”进行处理。
判断文件是否存在、判断文件是否有权限、打开文件、读(或者写)文件,那么把这些处理封装在一个函数中,它们之间的内聚就是“过程内聚”。
- 交互内聚
模块内部的元素之所以被划分在同一模块中,是因为这些元素都操作相同的数据。
交互内聚最常见的就是数据结构的类定义了,例如 Java HashMap 的 get、put、clear 等操作。
- 功能内聚
模块内部的元素之所以被划分在同一模块中,是因为这些元素都是为了完成同一项任务。
耦合
耦合和内聚是相反的:内聚关注模块内部的元素结合程度,耦合关注模块之间的依赖程度。
- 无耦合
无耦合意味着模块之间没有任何关系或者交互。
- 消息耦合
系统/子系统:两个系统交互的协议数据,例如:HTTP POST 数据,Java RPC、Socket 数据等;
类/函数:函数的参数即“消息”。例如:A 类的函数调用了 B 类的某个函数,传入的参数就是消息。
- 数据耦合
两个模块间通过参数传递基本数据,称为数据耦合。
- 数据结构耦合
数据结构耦合和数据耦合是比较相近的,主要差别在于数据结构耦合中传递的不是基本数据,而是数据结
构数据。
- 控制耦合
当一个模块可以通过某种方式来控制另外一个模块的行为时,称为控制耦合。
- 外部耦合
为了让香港的插头也能在大陆用,我们就需要买一个“电源转换头”,其作用就是能够让原来不兼容的香港插头和大陆插座两个设备能够连接起来,但原有两个设备内部都继续保持原样。
这里的香港插头和大陆插座就是外部耦合关系,外部设备是“电源转换头”
在软件系统,外部依赖最典型的莫过于各种“proxy”模块或者子系统了。比如说 A 系统输出 XML 格式,
B 系统只能接收 JSON 格式的数据,为了能够让两个系统连接起来,需要开发一个转换程序,完成格式转换。
简单来说,很多东西不是你想改就能改的!使用外部依赖一般的主要原因如下:
- 已有系统无法改动:比如说你买的香港插头和大陆插座,除非你真的是吃饱了没事做或者要挑战自己,否则一般人都不会自己动手将香港插头改为大陆插头,或者将大陆插座改为香港插座。
- 已有系统已经成熟,改动需要很高的成本,也会导致系统不稳定。
- 已有系统支持很多其它系统,不能为了你的系统单独修改。
高内聚低耦合
如果我们不做到这点,将会怎样?
- 如果一个模块内聚性较低,则这个模块很容易变化。一旦变化,设计、编码、测试、编译、部署的工作量就上来了,而一旦一个模块变化,与之相关的模块都需要跟着改变。
高内聚低耦合是否意味着内聚越高越好,耦合越低越好?
-
并不是内聚越高越好,耦合越低越好,真正好的设计
是在高内聚和低耦合间进行平衡,也就是说高内聚和低耦合是冲突的。 -
最强的内聚莫过于一个类只写一个函数,这样内聚性绝对是最高的。但这会带来一个明显
的问题:类的数量急剧增多,这样就导致了其它类的耦合特别多,于是整个设计就变成了“高内聚高耦合”了。由于高耦合,整个系统变动同样非常频繁。 -
对于耦合来说,最弱的耦合是一个类将所有的函数都包含了,这样类完全不依赖其它类,耦合性是最低的。但这样会带来一个明显的问题:内聚性很低,于是整个设计就变成了“低耦合低内聚”了。由于低内聚,整个类的变动同样非常频繁。
-
对于“低耦合低内聚”来说,还有另外一个明显的问题:几乎无法被其它类重用。原因很简单,类本身太庞大了,要么实现很复杂,要么数据很大,其它类无法明确该如何重用这个类。