洛译小筑

别来无恙,我的老友…
随笔 - 45, 文章 - 0, 评论 - 172, 引用 - 0
数据加载中……

[ECPP读书笔记 条目38] 使用组合来表示“A包含B”、“A以B的形式实现”

当一个类型A的对象中包含另一个类型B的对象时,我们说A与B之间的关系是“组合”。请看示例:

class Address { ... };             // 住址

 

class PhoneNumber { ... };

 

class Person {

public:

  ...

 

private:

  std::string name;                // 组合对象

  Address address;                 // 同上

  PhoneNumber voiceNumber;        // 同上

  PhoneNumber faxNumber;          // 同上

};

上述示例中,Person对象由stringAddressPhoneNumber三种对象组合而成。在程序员之间,组合一词拥有众多的同义词。诸如:分层包含聚合嵌入

条目32中解释了公共继承意味着“A是一个B”。同时组合也有其内涵,事实上它拥有两个内涵,组合既可以表示“A拥有一个B”,也可以表示“A以B的形式实现”。这是由于在你的软件中你针对的是两个不同的领域。你的程序中的一些对象与你正在建模的世界相关,比如人、车、视频帧等等。这些对象则存在于应用领域。而另一些对象单纯是为了程序的具体实现人为创造的,诸如缓冲区、互斥锁、搜索树,等等。这些类型的对象则针对软件中的实现领域。当组合出现在应用域内的对象之间时,它表达的是“A拥有一个B”的关系;而组合出现在实现域中,则意味着“A以B的形式实现”。

上文中的Person类演示了“A包含B”的关系。一个人——Person对象“拥有”一个姓名(name)、一个住址(address)、一个电话号码(voiceNumber)、一个传真号码(faxNumber)、你不能说姓名地址这样的话。应该说姓名”、“人住址”。大多数人还是能够轻易区分“是”和“有”之间的区别的。因此“A是B”和“A拥有B”两者并不易混淆。

某种意义上讲,“A是B”与“A以B的形式实现”二者之间的区别更让人难以分辨。举例说,假设你需要一个表示集合的模板,其中容纳的对象的数目非常有限。即该集合中不允许存在重复的对象。由于复用在OOP的世界里是十分美妙的事情,你会本能的想到使用标准库中的set模板。有现成的工具为什么不加以利用呢。

不幸的是,set模板的具体实现中一般每个元素都会有三个指针的开销。这是因为set通常被实现为平衡搜索树,这种数据结构确保了查找、插入、删除操作的时间复杂度均为O(lgn)在效率至上的环境中,这种设计方案合情合理,然而在你的程序中,空间比速度更加重要,这时标准库中的set模板就变得水土不服了。看上去你需要另起炉灶。

固然,复用的确是一件美妙的事情。作为数据结构专家的你,对于实现集合有各式各样的手段,其中之一便是使用链表。你当然也了解标准C++库中有一个list模板,因此你可以去(复)用它。

于是,你决定开辟一个全新的模板Set,由list模板继承而得。也就是说Set<T>将由list<T>继承而得。在你的实现中,Set对象实际上将会是一个list对象。于是你这样声明Set模板:

template<typename T>               // 创建Set:此处是复用list的错误做法

class Set: public std::list<T> { ... };

这一方按乍看上去十分完美,实际上却存有隐患。如条目32所讲,如果D是一个B,那么对于B成立的一切对于D也成立。然而,list对象中可以存在重复的元素,因此如果我们先后两次将3051这个值插入list<int>中,这个表中将存在两个3051的副本。相反,Set不应含有重复的元素,如果两次插入3051,那么Set<int>中应仅存在一个该值的副本。于是Set是一个list这一说法便不成立了——对于list对象成立的一些结论不适用于Set对象。

由于这两个类之间的关系不是“A是B,因此使用公共继承的方式来构造两者之间的关系便是错误的。我们可以想到Set对象可以list的形式实现,以下是正确的做法:

template<class T>                  // 创建Set:此处是复用list的正确做法

class Set {

public:

  bool member(const T& item) const;

  void insert(const T& item);

  void remove(const T& item);

  std::size_t size() const;

 

private:

  std::list<T> rep;                // 代表Set中的数据

};

Set的成员函数可以全方位的依赖list乃至标准库中其他部分提供的各项功能,因此实现方法是简单直接的,只要你掌握STL的基本使用方法即可:

template<typename T>

bool Set<T>::member(const T& item) const

{

  return std::find(rep.begin(), rep.end(), item) != rep.end();

}

 

template<typename T>

void Set<T>::insert(const T& item)

{

  if (!member(item)) rep.push_back(item);

}

 

template<typename T>

void Set<T>::remove(const T& item)

{

  typename std::list<T>::iterator it =

    std::find(rep.begin(), rep.end(), item);

                                   // 此处为何使用typename请参见条目42

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

}

 

template<typename T>

std::size_t Set<T>::size() const

{

  return rep.size();

}

这些函数足够简单,我们有理由将它们声明为内联函数,然而在你做出明确决定之前,我还是建议你去条目30复习一下内联的相关知识。

一些人可能会说:Set的接口应该更加遵守条目18中讨论的主题:设计接口要易于使用而不易误用,是否应该让Set遵守STL容器的标准,但是这里遵守这些标准需要为Set添加一大批内容,这样做会淹没它与list之间的关系。由于本章节讨论的中心是这一关系问题,因此这里我牺牲了STL的兼容性,而更多考虑了讲述的清晰程度。另外,Set接口的不完善并不会掩盖此处关于它的无须争辩的事实:其与list之间的关系并不是“A是B”(尽管乍看上去很像),而是“A以B的形式实现


时刻牢记

组合与公共继承之间存在着本质区别。

组合在应用领域意味着“A是B”,在实现领域意味着“A以B的形式实现”。

posted on 2012-07-08 16:18 ★ROY★ 阅读(1890) 评论(2)  编辑 收藏 引用 所属分类: Effective C++

评论

# re: 【读书笔记】[Effective C++第3版][第38条]使用组合来表示“A包含B”、“A以B的形式实现”  回复  更多评论   

LZ,最后一句是否有笔误?
2012-07-09 11:32 | qian

# re: 【读书笔记】[Effective C++第3版][第38条]使用组合来表示“A包含B”、“A以B的形式实现”  回复  更多评论   

@qian
改了改了~~看看这回行了不:)
2012-07-09 21:44 | ★ROY★

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理