在现实世界只是很容易区分对象之间是复合关系还是继承关系。没有人会说桔子有一个水果--而只能是桔子是一种水果。但是,在代码中,有时候就不是那么清晰了。
设想有一个代表关联数组的假想类,将一个键影射到一个值的数据结构。例如,一家保险公司可以使用一个AssociativeArray类将成员ID影射到一个名字,这样的话,给出一个ID,就会很容易地找到对应的成员的名字。成员ID就是键,成员名字就是值。
在标准关联数组实现中,一个键与一个值相关联。如果ID 12345影射到成员名字为“张三”,它就不能再影射到成员名字“李四”。在大部分实现中,如果你想对一个已经有值的键再加第二个值,原来的值就会消失。换句话说,如果ID 12345影射到了“张三”,你又给ID 12345赋了一个“李四”,那么张三就会马上脱保。这个可以以下顺序展示,对假设的insert()成员函数调用了再次,关联数组的内容结果如下:
myArray.insert(12345, "张三");
键 | 值 |
12345 | “张三”【字符串】 |
myArray.insert(12345, "李四");
键 | 值 |
12345 | “李四”【字符串】 |
使用像关联数组但是允许给定的键有多个值的数据结构并不难。像在保险的那个例子中,一个家庭可能有多个名字对应到了同一个ID。因为这样的一个数据结构与关联数组比较类似,去稍微修改一下其功能还是不错的。一个关联数组对于一个键只能有一个值,但这个值可以是任何值。除了字符串之外,也可以是一个集合(比如vector),一个键包含了多个值。对于一个已有的ID来讲,每次增加一个成员,就是给集合增加了一个名字。将以以下顺序起作用:
Collection collection; // 声名一个集合.
collection.insert("张三"); // 给集合添加一个新的元素.
myArray.insert(12345, collection); //将集合插入数组.
键 | 值 |
12345 | {“张三”}【集合】 |
Collection collection { myArray.get(12345) }; // 访问已有集合.
collection.insert("李四"); // 集合中增加一个新元素.
myArray.insert(12345, collection); // 用更新后的集合进行替换
键 | 值 |
12345 | {“李四”}【集合】 |
使用集合而不是字符串会比较讨厌,需要大量重复的代码。最好将这个多值功能封装到一个单独的类里面,可能会叫做MultiAssociativeArray,MultiAssociativeArray类与AssociativeArray比较像,除了背后的实现,它会存储集合的每一个字符串值而不是一个单独的字符串。当然了,MultiAssociativeArray在某种程度上与AssociativeArray相关联,因为它依然使用关联数组来保存数据。不清晰的是这种关系到底是复合关系还是继承关系。
我们先来说继承关系,先认为MultiAssociativeArray是AssociativeArray的继承类。最终我们会认为这是一个坏主意,但让我们做一个坏的设计的例子。MultiAssociativeArray必须覆盖成员函数,拉回数组的接入,这样的话,或者生成集合并添加成员,或者访问已有集合并添加成员。也必须覆盖访问值的成员函数。这就复杂了,虽然:覆盖后的get()成员函数需要返回一个单独的值,而不是一个集合。那MultiAssociativeArray应该返回什么呢?一个选择就是返回与键相关联的第一个值。可以加一个getAll()的成员函数来返回与键相关联的所有值。这比较像一个可信的设计。虽然它覆盖了基础类的所有成员函数,但它仍然使用了基础类的所有成员函数。下图为其UML类图。
现在再来考虑一下复合关系,MultiAssociativeArray是自身的类,但是它包含了一个AssociativeArray对象。它可能有一个与AssociativeArray相类似的接口,但是不需要一样。背后的实际情况是,当用户给MultiAssociativeArray添加东西时,实际是在集合中添加了一个AssociativeArray对象。这看起来很完美,如下图:
好了,哪种解决方案是对的?看起来没有正确答案,但大量的实践告诉我们,在这两种方法中复合关系通常更好。主要原因是不需要担心保持关联数组功能的情况下允许修改暴露在外的接口。例如,上面图示的get()成员函数就被修改成了getAll(),在MultiAssociativeArray中很清晰地对于给定的键获得了其所有值。还有,对于复合关系,你不需要担心关联数组功能的渗漏。例如,对于继承关系,如果关联数组支持一个能够获得值的总数的成员函数,它就会报告集合的数字,除非MultiAssociativeArray知道去覆盖它。但大家对这种渗露一般都不会太注意,会造成很大的问题。
也就是说,有人会提出一种这样的观点,MultiAssociativeArray实际上就是带有一些新的功能的AssociativeArray,那它应该是一种继承关系。这种观点认为在两种关系之间有时候有一条细线,你需要考虑类要怎样用,是否你要建立的就是从其他类中利用一些功能,还是确实是对类进行了修改或者增加了新功能。
以下列表列出了对MultiAssociativeArray类使用哪种方法的赞成和反对的意见:
继承关系 | 复合关系 | |
赞成原因 | 从基础上来说,是对于不同特点的相同的抽象。 提供了(绝大多数)与AssociativeArray相同的成员函数 | MultiAssociativeArray可以有任何有用的成员函数而不必担心AssociativeArray有的成员函数。 实现上可以进行任何除了AssociativeArray之外的修改,只要不改变对外暴露的成员函数。 |
反对原因 | 关联数组的定义就是一个键对着一个值。说MultiAssociativeArray是一个数组就是对数组的亵渎! MultiAssociativeArray覆盖了AssociativeArray的两个成员函数,这就是设计出了问题的强烈信号。 AssociativeArray的不知道或者不合适的属性或成员函数可能是对MultiAssociativeArray的渗漏。 | 某种意义上,MultiAssociativeArray通过新的成员函数重新造了轮子。 AssociativeArray的一些增加的属性与成员函数可能会比较有用。 |
在这个案例中不使用继承关系的原因是很站得住脚的。还有,利斯科夫取代原理(LSP)可以帮助你在使用继承与复合关系之间做出决定。该原理指出,在不改变行为的前提下是可以使用继承类而不是基类的。应用到这个例子中,它指出这一定是一个复合关系,因为在使用AssociativeArray之前无法直接使用MultiAssociativeArray。如果你硬要这么干的话,行为就会发生改变。例如,AssociativeArray的insert()成员函数对于在数组中已经存在的同样的键改掉了原来的值,而MultiAssociativeArray不会改掉这些值。
以上两种解决方案实际上不是仅有的可能的解决方案。其他的选择可以是让AssociativeArray包含一个MultiAssociativeArray,或者是两者都继承于一个通用的基类,等等。对于特定的设计你可以想出多种解决方案。
如果你必须要在这两种关系之间进行选择,我还是推荐,根据多年的经验,使用复合关系吧。
记住,在这儿展示继承关系与复合关系之间的不同的AssociativeArray与MultiAssociativeArray,在你自己的代码中,推荐使用标准关联数组类而不是要自己去写。C++标准库提供了std::map可以代替AssociativeArray,std::multimap可以代替MultiAssociativeArray。