3.4.3 "有一个"与"是一个"的区别
在现实中,区分对象之间的"有一个"与"是一个"关系相当容易。没人会说桔子有一个水果-- 桔子是一种水果。在代码中,有时候并不会这么明显。
考虑一个代表哈希表的假想类,哈希表是高效地将键映射到值的一种数据结构。例如,保险公司使用Hashtable类将成员ID映射到名称,从而给定一个ID就可以方便地找到对应的成员名称。成员ID是键,成员名称是值。
在标准哈希表实现中,每个键都有一个值。如果ID 14534映射到名称"Kleper,Scott",就不能再映射到成员名称"Kleper,Marni"。在大多数实现中,如果对一个已经有值的键添加第二个值,第一个值就会消失。换句话说,如果ID 14534映射到"Kleper,Scott",然后又将ID 14534分配给"Kleper,Marni",那么Scott将被遗弃,下面的序列两次调用了假想哈希表enter()行为,并给出了每次调用结束后哈希表的内容。hash.enter用到了超前一点的C++(www.cppentry.com)对象语法,可以将其当作"使用hash对象的enter行为"。
- hash.enter(14534, "Kleper, Scott");
|
键< xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> |
值 |
|
14534 |
"Kleper, Scott" [字符串] |
- hash.enter(14534, "Kleper, Marni");
|
键 |
值 |
|
14534 |
"Kleper, Marni" [字符串] |
不难想象类似于哈希表但是允许一个键有多个值的数据结构的使用。在保险公司示例中,一个家庭可能有多个名称对应于同一个ID。由于这种数据结构非常类似于哈希表,因此可以用某种方式使用哈希表的功能。哈希表的键只能有一个值,但是这个值是任意类型。除了字符串之外,这个值还可以是一个包含多个键值的集合(例如数组或者列表)。当向已有ID添加新的成员时,可以将名称加入到集合中。运行方式如下所示:
- Collection collection; // Make a new collection.
- collection.insert("Kleper, Scott"); // Add a new element to the collection.
- hash.enter(14534, collection); // Enter the collection into the table.
|
键 |
值 |
|
14534 |
{“Kleper, Scott”}[集合] |
- Collection collection = hash.get(14534);// Retrieve the existing collection.
- collection.insert(“Kleper, Marni”); // Add a new element to the collection.
- hash.enter(14534, collection); // Replace the collection with the updated one.
|
键 |
值 |
|
14534 |
{“Kleper, Scott”, “Kleper,
Marni”} [集合] |
使用集合而不是字符串有些繁琐,并且需要大量重复代码。在一个单独的类中封装多值功能应该会比较好,可以将这个类叫做MultiHash。MultiHash类的运行与Hashtable类似,只是暗地里将每个值作为字符串的集合而不是单个字符串存储。很明显,MultiHash与Hashtable有某种联系,因为它仍然使用哈希表存储数据。不明显的是,这是"是一个"关系还是"有一个"关系。
先考虑"是一个"关系。假定MultiHash是Hashtable的子类,它必须重写在表中添加项的行为,从而既可以创建集合并添加新的元素,又可以获取已有集合并添加新的元素。此外还必须重写获取值的行为。例如,可以将给定键的所有值集中到一个字符串。这好像是一个相当合理的设计。即使子类重写了超类所有的行为,仍然可以在子类中使用原始行为,从而使用超类的行为。这种方法如图3-5所示。
|
| 图 3-5 |
现在考虑"有一个"关系。MultiHash属于自己的类,但是包含了Hashtable对象。这个类的接口可能与Hashtable非常相似,但是并不需要相同。在幕后,当用户向MultiHash添加项时,会将这个项封装到一个集合并送入Hashtable对象。这也很合理,如图3-6所示。
|
| 图 3-6 |
那么,哪个方案是正确的?没有明确的答案,本书的作者之一认为这是"有一个"关系,他编写了一个MultiHash类供产品使用。主要原因是允许修改公开的接口而不需要担心维护哈希表的功能。例如,图3-6中get行为变成了getAll,清楚表明将获取MultiHash中某个特定键所有的值。此外,在"有一个"关系中,不需要担心哈希表功能会渗透。例如,如果哈希表类提供了获取值的总数的方法,只要MultiHash不重写这个方法,就可以用这个方法报告集合的数目。
这就是说,MultiHash实际上是一个具有新功能的Hashtable这一说法是让人信服的,因此应该是"是一个"关系。关键在于有时候这两种关系之间的差别很小,您需要考虑使用类的方式,还需要考虑您创建的类只是利用了其他类的一些功能,还是在其他类的基础上修改或者添加新功能。
表3-3给出了关于MultiHash两种方法的支持以及反对意见。
表 3-3
|
|
是一个 |
有一个 |
|
支持的原因 |
● 基本上,这是具有不同特征的同一抽象
● 这个类的行为与Hashtable(几乎)相同 |
● MultiHash可以拥有任何有用的行为,
而不需要考虑Hashtable拥有什么行为
● 可以不采用Hashtable实现方式,
同时不需要改变公开的行为 |
|
反对的原因 |
● 根据定义,哈希表一个键对应一个值,
将MultiHash当作哈希表是错误的
● MultiHash将哈希表的两个行为全部重写,
这有力地说明这个设计是错误的
● Hashtable未知的或者不正确的属性以
及行为会“渗透”到MultiHash |
● 在某种意义上,MultiHash通
过提出新行为进行了重造
● Hashtable的一些其他属性
以及行为可能是有用的 |
反对"是一个"关系的理由在这种情况下非常有力。实际上,根据作者多年的经验,如果要选择的话建议采用"有一个"关系而不是"是一个"关系。
注意,在这里使用Hashtable和MultiHash说明了"有一个"和"是一个"关系的不同。在代码中,建议使用标准哈希表类而不是自己写一个。C++(www.cppentry.com)11的标准库中提供了一个unordered_map类,用来代替Hashtable,此外还提供了一个unordered_multimap类,可以用来代替MultiHash类。在第12章将讨论这两个标准类。