2.3 类耦合与内聚
OOD启思录
一些经验原则用于解决类的耦合与内聚问题。我们努力让类更紧密地内聚,并尽量降低类间耦合程度。这和在面向动作范型中试图让函数更紧密地内聚并尽量降低函数间的耦合程度的努力是一致的。函数中的紧密内聚意味着组成函数的所有代码都是紧密相关的。函数间的松耦合意味着当一个函数想要使用另一个函数时,它应当在总是从同一点进入该函数,并从同一点退出。这样,我们就可以得出这样的面向动作的经验原则:“函数应当只有一条返回语句。”
在面向对象范型中,我们把松耦合和紧内聚的目标映射到了类的层次。类之间有5种形式的耦合关系:零耦合(nil coupling)是最佳的,因为这意味着两个类丝毫不依赖于对方。你可以去掉一个类,而不会影响另一个。当然,如果只用到零耦合,你无法创建有意义的应用程序。若只用到零耦合,我们最多只能创建类库,这样的类库由一系列的独立类组成,这些类相互之间没有影响。导出耦合(export coupling)则表明,一个类依赖于另一个类的公有接口。1也就是说,这个类用到另一个类的一个或多个公有操作。授权耦合(overt coupling)则意味着一个类经允许使用另一个类的实现细节。C++的友元机制是授权耦合的典型例子。一个C++类X可以声明类Y是它的友元。这样,Y的方法就获得授权可以访问X的实现细节。自行耦合(covert coupling)和授权耦合差不多,也是类Y访问类X的实现细节,但区别在于类Y是未经授权的。如果我们发明一种语言机制,允许类Y声明自身是X的友元并且将使用X的实现细节,那么X和Y就是自行耦合的。最后一种耦合是暗中耦合(surreptitious coupling),这种耦合是指类X通过某种方式知道了Y的实现细节。如果类X使用类Y的公有数据成员,那么X就和Y暗中耦合。暗中耦合是最危险的耦合形式,因为它在Y的行为和X的实现之间建立了很强的隐式依赖关系。
经验原则2.7
类之间应该零耦合,或者只有导出耦合关系。也即,一个类要么同另一个类毫无关系,要么只使用另一个类的公有接口中的操作。
所有其他形式的耦合都允许类把实现细节暴露给其他类,这样就在两个类的实现之间建立了隐含依赖关系。将来如果一个类想要修改它的实现,那么这些隐含依赖关系总会带来维护问题。
类内聚努力确保类内部的所有元素都是紧密关联的。有一些经验原则牵涉到这一属性。
经验原则2.8
类应当只表示一个关键抽象。
一个关键抽象(key abstraction)被定义成领域模型中的一个主要实体。关键抽象经常以名词形式出现,并伴随着需求规约。每个关键抽象都应当只映射到一个类。如果它被映射到多个类,那么设计者可能是把每个功能都表示为一个类了。如果多个关键抽象被映射到了同一个类,那么设计者可能在创建一个集中化的系统。这些类经常被称为含糊的类(vague classes),并且需要分割成两个或多个类,每个类表示一个关键抽象。第3章我们将更详尽地探讨这两种不良设计。
经验原则2.9
把相关的数据和行为集中放置。
如果违反这条经验原则,那么开发者就不得不按以往方式编程。为了实现单一的系统需求,开发者不得不改动系统的两处或者多处。其实这两处(或者多处)是同一个关键抽象,所以应当用同一个类表示。设计者应当留意那些通过get之类操作从别的对象中获取数据的对象。这种类型的行为暗示着这条经验原则被违反了。考虑一下一个烤炉类的使用者想要在烧烤之前预热烤炉。用户应当只需要发送给烤炉一条are_you_preheated?()消息就可以了。烤炉应当可以测试自己的温度是否已经达到了需要的温度,并且测试其他预热需要满足的条件。如果用户为了知道烤炉是否已经预热,需要问烤炉目前温度、期待温度、燃气阀的状态、常燃火状态等等,那么就违反了这条经验原则。烤炉拥有这些温度和燃气烹饪设备的信息,它应当自行判断这个对象是否已经预热了。留意那些为了实现不正确的预热方式而需要用到的get方法(比如,get_actualtemp()、get_desiredtemp()、get_valvestatus()等等)是很重要的。
经验原则2.10
把不相关的信息放在另一个类中(也即:互不沟通的行为)。
开发者应当留意这样的类:方法的一个子集操作数据成员的一个真子集2。极端情况是,一个类有一半方法操作一半数据成员,另一半方法则操作另一半数据成员(见图2.6)。
这是一个更接近现实世界的例子。请考虑词典类。对于小型词典,最好的实现是属性列表(单词和它们定义的列表),但是对大型词典来说,哈希表更好(更快)。两种辞典的实现都需要提供增加单词和寻找单词的能力。图2.7展示了一个具有互不沟通的行为的词典类设计。
这个解决方案假设词典类的使用者知道词典将会有多大。他们需要做出决定,是使用哈希表实现的词典还是链表实现的词典。一般而言,在类名中显示实现细节并让用户来做这样的选择不是好主意。一个更好的解决方案留在第5章讲述,因为它要用到继承。在那个解决方案中,一个单一的词典类把它的实现隐藏为内部细节。如果词典的大小增长到了一个事先定下的临界值,词典类会决定改变实现。
1译注:在Eiffel中可以显式地声明导出关系。
2译注:子集(subset)和真子集(proper subset)的区别在于,一个集合是其本身的子集,但不是其本身的真子集。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。