"如果一个系统即复杂且高耦合, 则一定会发生许多意料之外的事情"
原则: 避免出现大型的复杂模块, 因为模块过大功能过多往往意味着模块间的紧耦合.
解决之道: 为每个模块安排单一的职责, 且使用接口将实现进行隐藏.
好处: 代码会变得更易维护. 且代码更易进行测试和使用.
之前的 4 个原则都是针对代码单元而言的, 意味着在实践中应用它们可以让独立的代码单元(方法/构造函数)更易维护.
从这里开始, 就转而关注模块(Module)层面上的内容了.
注意: 这里的"模块"在面向对象语言中指的是一个类, 比如 Java 中的一个 class. 即之后的模块层面上的原则都是为了解决类之间的关系问题的.
而本条原则的主旨是: 解决类之间的耦合问题, 总地来说就是分离关注点.
设计一个复杂的软件系统需要考虑很多,每一个需要考虑的方面可以称之为一个关注点(Concern),良好的设计需要把这些关注点分门别类,划分为若干模块,让程序开发人员在处理一个关注点时可以尽可能少的被其他关注点的细节所干扰。模块化软件开发就是一种分离关注点(Separation of Concerns)的手段,模块化应当遵循高内聚、低耦合的原则,提高模块的独立性。
这里遇到一个 fan-in
的概念, 软件设计中,扇入和扇出的概念是指应用程序模块之间的层次调用情况。
按照结构化设计方法,一个应用程序是由多个功能相对独立的模块所组成。
扇入:是指直接调用该模块的上级模块的个数。扇入大表示模块的复用度高。
扇出:是指该模块直接调用的下级模块的个数。扇出大表示模块的复杂度高,需要控制和协调过多的下级模块;但扇出过小(例如总是1)也不好。扇出过大一般是因为缺乏中间层次,应该适当增加中间层次的模块。扇出太小时可以把下级模块进一步分解成若干个子功能模块,或者合并到它的上级模块中去。
设计良好的软件结构,通常顶层扇出比较大,中间扇出小,底层模块则有大扇入。
1 一个实际的例子
某个类最开始只有读取用户, �修改用户, 判断用户是否存在这三个功能. 但没有使用接口进行隐藏实现. 而后随着不断添加新的需求, 不断迭代, 导致类的功能不断增加, 形成了一个大型类, 而且该类又被多个地方调用(超过 50 处), 即大扇入. 结果就是这个类变成一个紧耦合的类, 因为它被大量的上层模块调用, 且没有接口, 而是直接在实现上进行调用, 而且它也知道许多其他类的内部情况.(比如它可以调用不同的数据访问层模块来完成不同的功能, 而这些调用也是在实现上调用, 而非调用接口.)
模块间的耦合指的是当改变发生的时候, 却发现两个或多个系统部分是连接在一起的. 就像是你想拆掉发动机, 却发现发动机和变速箱焊到了一起解不开了.
这种紧耦合的结果就是它们变为了可维护性提升道路上的绊脚石. 而上面说的这个大型类也就是没有合理分离关注点才形成的.
而这样的大型类后面也就越来越难理解, 甚至变得无法管理.
�总结一下, 为什么类之间的耦合需要高度重视, 主要是因为:
- 耦合本身就是一个模块层面上的代码问题.
- 耦合是绝对存在的, 但它有程度的区别, 我们要做到的是尽量低耦合. 而耦合的程度是通过
number of call
和size of that class
共同来衡量, 即外界的调用数量(扇入), 以及这个类的大小. 比如外界若调用这个类的次数更多, 那这个类就应该要更小.
从整个程序的维度来看, �上层模块的扇出(调用其他模块的次数)应该是大的, 而底层模块的扇入(被其他模块调用的次数)应该是大的, 所以保证低耦合的手段就是尽量减小类的体积. 而减小类体积的方法就是对关注点进行合理分离(�假设在代码单元层面上的可维护原则已经得到合理应用), 另外就�是使用接口来隐藏实现, 从而达到低耦合.
2 动机
- 更小的, 更低耦合的模块让多人开发更加容易, 并且变更的时候更加容易进行.
- 更小的, 更低耦合的模块更加容易定位.
- 更小的, 更低耦合的模块对所有开发者都更加友好.
3 做法
- 将关注点进行合理分离, 从而减小类的体积. 即模块的功能应该是单一的, 这需要对关注点进行合理分离.
- 利用接口来隐藏实现.
- 将代码替换为 Library 或 Framework, 这样可以保证在一处维护代码而让多处使用, 这样也可以保证模块间的低耦合.