Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1755709
  • 博文数量: 600
  • 博客积分: 10581
  • 博客等级: 上将
  • 技术积分: 6205
  • 用 户 组: 普通用户
  • 注册时间: 2008-11-06 10:13
文章分类
文章存档

2016年(2)

2015年(9)

2014年(8)

2013年(5)

2012年(8)

2011年(36)

2010年(34)

2009年(451)

2008年(47)

分类: C/C++

2009-09-19 13:39:57

使某个类的对象成为另一个类的数据成员,从而实现将一个类构筑在另一个类之上,这一过程称为 "分层"(Layering)。例如:

class Address { ... };           // 某人居住之处

class PhoneNumber { ... };

class Person {
public:
  ...

private:
  string name;                   // 下层对象
  Address address;               // 同上
  PhoneNumber voiceNumber;       // 同上
  PhoneNumber faxNumber;         // 同上
};

本例中,Person类被认为是置于string,Address和PhoneNumber类的上层,因为它包含那些类型的数据成员。"分层" 这一术语有很多同义词,它也常被称为:构成(composition),包含(containment)或嵌入(embedding)。

条款35解释了公有继承的含义是 "是一个"。对应地,分层的含义是 "有一个" 或 "用...来实现"。

上面的Person类展示了 "有一个" 的关系。一个Person对象 "有一个" 名字,地址,电话号码和传真号码。你不能说,一个人 "是一个" 名字或一个人 "是一个" 地址;你得说,一个人 "有一个" 名字, "有一个" 地址,等等。大多数人对区分这些没什么困难,所以混淆 "是一个" 和 "有一个" 的情况相对来说比较少见。

稍微有点麻烦的是区分 "是一个" 和 "用...来实现"。例如,假设需要一个类模板,用来表示任意对象的集合,并且集合中没有重复元素。程序设计中,重用(Reuse)是再好不过的一件事了,而且你也许已经读过条款49中关于C++标准库的总体介绍,那么,你的第一反应一定是想采用标准库中的set模板。是啊,既然可以使用别人所写的东西,为什么还要再去写一个新的模板呢?

但是,深入研究set的帮助文档后,你会发现,set的下述限制将不能满足你的程序要求:set要求包含在它内部的元素必须是完全有序的,即,对set中的任两个元素a和b来说,一定可以确定:要么a

当然,重用还是件好事。作为数据结构专家,你知道,在实现集合的众多选择中,一个最简单的办法是采用链表。你一定猜到了什么。对,标准库中正有这么一个list模板(用来产生链表类)!所以可以重用它。

具体来说,你决定让自己的Set模板从list继承。即,Set将从list继承。因为,在你的实现中,Set对象实际上将是list对象。于是你这样声明Set模板:

// Set中错误地使用了list
template
class Set: public list { ... };

至此,一切好象都很正确,但实际上错误不小。正如条款35所说明的,如果D "是一个" B,对B成立的所有事实对D也成立。但是,list对象可以包含重复元素,所以如果3051这个值被增加到list中两次,list中将包含3051的两个拷贝。相反,Set不可以包含重复元素,所以如果3051被增加到Set中两次,Set中将只包含这个值的一个拷贝。于是,说一个Set "是一个" list就是弥天大谎,因为如上所述,有一些在list对象中成立的事实在Set对象中不成立。

因为这两个类的关系并非 "是一个",所以用公有继承来表示它们的关系就是一个错误。正确的方法是让Set对象 "用list对象来实现":

// Set中使用list的正确方法
template
class Set {
public:
  bool member(const T& item) const;

  void insert(const T& item);
  void remove(const T& item);

  int cardinality() const;

private:
  list rep;                       // 表示一个Set
};

Set的成员函数可以利用list以及标准库其它部分所提供的大量功能,所以,实现代码既不难写也很易读:

template
bool Set::member(const T& item) const
{ return find(rep.begin(), rep.end(), item) != rep.end(); }

template
void Set::insert(const T& item)
{ if (!member(item)) rep.push_back(item); }

template
void Set::remove(const T& item)
{
  list::iterator it =
    find(rep.begin(), rep.end(), item);

  if (it != rep.end()) rep.erase(it);
}

template
int Set::cardinality() const
{ return rep.size(); }

这些函数很简单,所以很自然地想到将它们作为内联函数;但在做最后决定前,还是回顾一下条款33所做的讨论。(上面的代码中,find, begin, end, push_back等函数是标准库基本框架的一部分,它们可用来对list这样的容器模板进行操作。标准库框架的总体介绍参见条款49和M35。)

值得指出的是,Set类的接口没有做到完整并且最小(参见条款18)。从完整性上来说,它最大的遗漏在于不能对Set中的内容进行循环,而这一功能对很多程序来说是必需的(标准库中的所有成员都提供了这一功能,包括set)。Set的另一个缺陷是没有遵循标准库所采用的容器类常规(见条款49和M35),从而造成使用Set时更难以利用库中其它的部分。

Set的接口尽管有这些瑕疵,但下面这一点不能被掩盖:Set在理解它和list的关系上,具有无可辩驳的正确性。这种关系并非 "是一个"(虽然初看会以为是),而是 "用...来实现",通过分层来实现这种关系是类的设计者应该感到自豪的。

顺便说一句,当通过分层使两个类产生联系时,实际上在两个类之间建立了编译时的依赖关系。关于为什么要考虑到这一点以及如何减少这方面的麻烦,参见条款34。

阅读(457) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~