模块的独立性很重要,因为有效的模块化(即具有独立的模块)的软件比较容易开发出来。
独立的模块比较容易测试和维护。模块的独立程度可以由两个定性标准度量,这两个标准分别称为内聚和耦合。

前言

评价设计质量的两个条件:聚合和耦合
内聚,指一个模块内各个元素彼此结合的紧密程度;耦合指一个软件结构内不同模块之间互连程度的度量。
内聚意味着重用和独立,耦合意味着多米诺效应牵一发动全身。

好的设计,应该具有高聚合,低耦合的特点。

在面向对象的设计中,有很多设计模式可以促进聚合最大化的减少无需的耦合,诸如 MVC.

聚合

如果一个实体执行的是一个单一的,定义明确的任务,和它有关的一切都是执行这个任务的必要条件,那么我们说这个实体是聚合的。

我们可以在实体上定义聚合,这个定义也可以延伸到类,向低层次延申可以到类中某个方法,向高层次延申可以到包,甚至更高层次的子系统,系统。

  1. 好的设计目标就是每一个设计的实体都有最高可能的聚合。
  2. 一个聚合的实体功能,可以用简单的一句话来描述。

聚合分类

理想的聚合

  • 功能聚合

实体执行的是单一的,定义明确的任务,无任何副作用。高聚合的方法应该是功能聚合的。

  • 信息内聚

实体代表一组聚合的数据和在这些数据上独立的操作。高聚合的类应该是信息内聚的。

次理想的聚合

  • 沟通的、连续的、程序性的聚合

实体执行的是必须按照一定顺序执行的一系列任务。

  • 时间上的聚合

实体执行的是必须在同一时间完成的任务,比如初始化操作,或者清理释放操作。

  • 逻辑上的聚合

实体负责的是一组相关的任务,由调用方决定哪些任务被执行,一般定义多个实体或者使用多态。

  • 实用的聚合

实体负责的是一组相关的任务,但也仅此而已。比如 java.util 包和 java.Math 类。这种次理想的聚合是不可避免的。他们会最“聚合”的情况也就如此了。

不理想的聚合

实体负责的是一组无关的,只是因为巧合放在一起的任务。

聚合改进

任何有更好聚合方案的实体,都应该被改进。

  • 整理实体功能,修改功能描述,依据功能描述开发功能。

是不是能够用简单的一句话概括这个实体的功能(without and),如果不能,尝试换用新的语言概括这个功能。

举例子:一个方法具有 下楼,骑车,坐地铁,到公司,打卡 等功能,概括为 去上班。

  • 拆分

将一个低聚合的实体拆分成多个内部具有高聚合的实体。

举例子:一个方法具有 下楼,骑车,做地铁,到公司,到公园,打卡,自拍 等功能。拆分成两个,去上班 和 去游玩。

耦合

耦合是对一个软件结构内不同模块之间互连程度的度量。耦合强弱取决于模块间接口的复杂程度,进入或访问一个模块的点,以及通过接口的数据。

系统的耦合程度取决于内部各个组件间的耦合程度。有些情况,耦合是必要的。我们需要做的就是去除不必要的耦合,这样对于系统的维护和修改就更容易。

识别耦合

  1. A概括了或者实现了B
  2. A通过方法依赖了B
    • 有局部变量B
    • 有参数B
    • 有返回值B
  3. 两个结论
    • 重用A需要也重用B
    • 修改B也需要修改A
  4. 依赖是不可避免的,而且经常是必须的,要做的就是降低耦合避免复合修改(修改B也得修改A)

耦合缺点

  1. 一个模块的修改会产生涟漪效应,其他模块也需随之修改。
  2. 由于模块之间的依赖关系,模块的组合会需要更多的精力及时间。
  3. 由于一个模块依赖很多其他模块,模块的可复用性会降低。

耦合分类和改进

模块耦合分为数据耦合、特征耦合、控制耦合、公共环境耦合、内容耦合。耦合程度由低到高。

数据耦合

两个模块彼此间通过参数交换信息,而且交换的信息仅仅是数据(指的是单一的原子的数据片段),那么这种耦合称为数据耦合。数据耦合是低耦合。系统中至少必须存在这种耦合。这是不可避免的。

特征耦合

当把整个数据结构作为参数、局部变量或者返回值,而被调用的模块只需要使用其中一部分数据元素时,就出现了特征耦合。

比如判断一个人是否成年,只需要传递该人的出生日期即可,不必要把人传进去。

因此这种耦合,通常是可以通过修改参数、局部变量或者返回值,只使用必要的数据元素来避免。

控制耦合

传递的信息中有控制信息(尽管有时这种控制信息以数据的形式出现),则这种耦合称为控制耦合。控制耦合是中等程度的耦合。

比如,传递的参数中包含一个枚举类型的参数,在方法的具体逻辑中判断该参数实现不同的功能。
那么,完全可以通过将一个方法修改为多个方法改进这种耦合。

公共环境耦合

当两个或多个模块通过一个公共数据环境相互作用时,它们之间的耦合称为公共环境耦合。
公共环境可以是全程变量、共享的通信区、内存的公共覆盖区、任何存储介质上的文件、物理设备等。
公共环境耦合的复杂程度随耦合的模块个数而变化,当耦合的模块个数增加时复杂程度显著增加。

只有两个模块有公共环境,耦合有下面两种可能。

  1. 一个模块往公共环境送数据,另一个模块从公共环境取数据。这是数据耦合的一种形式,是比较松散的耦合。
  2. 两个模块都既往公共环境送数据又从里面取数据,这种耦合比较紧密,介于数据耦合和控制耦合之间。

限制范围,比如如果是依赖的是汇率,提供一个统一获汇率配置的方法,限制耦合范围。

内容耦合

最高程度的耦合是内容耦合。如果出现下列情况之一,两个模块间就发生了内容耦合。

  1. 一个模块访问另一个模块的内部数据。
  2. 一个模块不通过正常入口而转到另一个模块的内部。
  3. 两个模块有一部分程序代码重叠(只可能出现在汇编程序中)。
  4. 一个模块有多个入口(这意味着一个模块有几种功能)。

应该坚决避免使用内容耦合,重构吧。

耦合总结

总之,耦合是影响软件复杂程度的一个重要因素。
应该采取下述设计原则:
尽量使用数据耦合,少用控制耦合和特征耦合,限制公共环境耦合的范围,完全不用内容耦合。

附录 一些软件设计的相关原则

当在两个或多个地方发现一些相似的代码的时候,它们的共性抽象出来形成一个唯一的新方法,并且改变现有地方的代码让它们以一些合适的参数调用这个新的方。
可以说是在我们的软件开发中最常使用的原则,也最容易理解。
前端时间前端圈(JS圈?)还在围着这个做讨论我不是很懂 Node.js 社区的 DRY 文化

在界面、操作、交互设计上,简单的东西总比复杂的更容易让人接受。甚至从家装到商业风格,都有这样的实践。

  • Program to an interface, not an implementation

注重接口而不是实现,依赖接口而不是实现,是设计模式中最根本的哲学。

tip. 面向接口而不是实现,喜欢组合而不是继承 是 23个经典设计模式的设计原则

只考虑和设计必须的功能,避免过度设计。只实现目前需要的功能,在以后需要更多功能时,可以再进行添加。软件开发是 trade-off 的博弈。

又称“最少知识原则”(Principle of Least Knowledge)

More formally, the Law of Demeter for functions requires that a method m of an object O may only invoke the methods of the following kinds of objects:
O itself
m's parameters
Any objects created/instantiated within m
O's direct component objects
A global variable, accessible by O, in the scope of m

如果你想让你的狗跑的话,你会对狗说还是对四条狗腿说?
use only one dot

  • 面向对象的S.O.L.I.D 原则

    • SRP Single responsibility principle 单一职责原则。一个类,只做一件事,并把这件事做好,其只有一个引起它变化的原因。单一职责原则可以看作是低耦合、高内聚在面向对象上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
    • OCP Open/Closed Principle 开闭原则。模块是可扩展的而不可更改的,对扩展开放,如果新的需求或者变化,可以对代码进行扩展,以适应新的情况;对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,不要对类进行任何修改。
    • LSP Liskov Substitution Principle 里氏代换原则 Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
    • ISP Interface Segregation Principle 接口隔离原则 Many client-specific interfaces are better than one general-purpose interface.
    • DIP Dependency Inversion Principle 依赖倒置原则 depend upon abstractions, not concretions. 依赖于抽象而不是实现。
  • CCP Common Closure Principle 共同封闭原则

一个包中所有的类应该对同一种类型的变化关闭。一起修改的类,应该组合在一起(同一个包里)。扩展了 OCP 的“关闭”概念。

  • CRP Common Reuse Principle 共同重用原则

包的所有类被一起重用。如果你重用了其中的一个类,就重用全部。没有被一起重用的类不应该组合在一起。

CCP 让包尽可能大(CCP 原则加入功能相关的类),CRP 则让包尽可能小(CRP 原则剔除不使用的类)。它们的出发点不一样,但不相互冲突。

  • 好莱坞原则 Hollywood Principle

“Don’t call us, we’ll call you.” 意思是,好莱坞的经纪人不希望你去联系他们,而是他们会在需要的时候来联系你。也就是说,所有的组件都是被动的,所有的组件初始化和调用都由容器负责。

简单来讲,就是由容器控制程序之间的关系,而非传统实现中,由程序代码直接操控。这也就是所谓“控制反转”的概念所在:1) 不创建对象,而是描述创建对象的方式。2)在代码中,对象与服务没有直接联系,而是容器负责将这些联系在一起。控制权由应用代码中转到了外部容器,控制权的转移,是所谓反转。好莱坞原则就是IoC(Inversion of Control) 或DI(Dependency Injection)的基础原则。

  • 高内聚,低耦合 High Cohesion & Low/Loose coupling

这个原则是 UNIX 操作系统设计的经典原则,把模块间的耦合降到最低,而努力让一个模块做到精益求精。
内聚,指一个模块内各个元素彼此结合的紧密程度;耦合指一个软件结构内不同模块之间互连程度的度量。
内聚意味着重用和独立,耦合意味着多米诺效应牵一发动全身。

  • CoC (Convention over Configuration) 惯例优于配置原则

简单点说,就是将一些公认的配置方式和信息作为内部缺省的规则来使用。
前段时间的 maven 配置修改源码路径,以及一些 maven 本身的配置。

  • SoC (Separation of Concerns) 关注点分离

SoC 是计算机科学中最重要的努力目标之一。这个原则,就是在软件开发中,通过各种手段,将问题的各个关注点分开。如果一个问题能分解为独立且较小的问题,就是相对较易解决的。问题太过于复杂,要解决问题需要关注的点太多,而程序员的能力是有限的,不能同时关注于问题的各个方面。

正如程序员的记忆力相对于计算机知识来说那么有限一样,程序员解决问题的能力相对于要解决的问题的复杂性也是一样的非常有限。在我们分析问题的时候,如果我们把所有的东西混在一起讨论,那么就只会有一个结果——乱。实现关注点分离的方法主要有两种,一种是标准化,另一种是抽象与包装。标准化就是制定一套标准,让使用者都遵守它,将人们的行为统一起来,这样使用标准的人就不用担心别人会有很多种不同的实现,使自己的程序不能和别人的配合。

就像是开发镙丝钉的人只专注于开发镙丝钉就行了,而不用关注镙帽是怎么生产的,反正镙帽和镙丝钉按照标准来就一定能合得上。不断地把程序的某些部分抽象并包装起来,也是实现关注点分离的好方法。一旦一个函数被抽象出来并实现了,那么使用函数的人就不用关心这个函数是如何实现的。同样的,一旦一个类被抽象并实现了,类的使用者也不用再关注于这个类的内部是如何实现的。诸如组件、分层、面向服务等这些概念都是在不同的层次上做抽象和包装,以使得使用者不用关心它的内部实现细节。

  • DbC(Design by Contract)契约式设计

DbC 的核心思想是对软件系统中的元素之间相互合作以及“责任”与“义务”的比喻。这种比喻从商业活动中“客户”与“供应商”达成“契约”而得来。

如果在程序设计中一个模块提供了某种功能,那么它要:

  1. 期望所有调用它的客户模块都保证一定的进入条件:这就是模块的先验条件(客户的义务和供应商的权利,这样它就不用去处理不满足先验条件的情况)。

  2. 保证退出时给出特定的属性:这就是模块的后验条件(供应商的义务,显然也是客户的权利)。

  3. 在进入时假定,并在退出时保持一些特定的属性:不变式。

  • ADP(Acyclic Dependencies Principle 无环依赖原则

包(或服务)之间的依赖结构必须是一个直接的无环图形,也就是说,在依赖结构中不允许出现环(循环依赖)。如果包的依赖形成了环状结构,怎么样打破这种循环依赖呢?

有两种方法可以打破这种循环依赖关系:第一种方法是创建新的包,如果 A、B、C 形成环路依赖,那么把这些共同类抽出来放在一个新的包 D 里。这样就把 C 依赖 A 变成了 C 依赖 D 以及 A 依赖 D,从而打破了循环依赖关系。第二种方法是使用 DIP(依赖倒置原则)和 ISP(接口分隔原则)设计原则。无环依赖原则(ADP)为我们解决包之间的关系耦合问题。在设计模块时,不能有循环依赖。