Fork me on GitHub

读书笔记 effective c++ Item 38 通过组合(composition)为 “has-a”或者“is-implemented-in-terms-of”建模

 

1. 什么是组合(composition)?

组合(composition)是一种类型之间的关系,这种关系当一种类型的对象包含另外一种类型的对象时就会产生。举个例子:

 

 1 class Address { ... };      // where someone lives
 2 
 3 class PhoneNumber { ... };
 4 class Person {
 5 public:
 6 ...
 7 private:
 8 std::string name;          // composed object
 9 
10 
11   Address address; // ditto
12   PhoneNumber voiceNumber; // ditto
13 
14 PhoneNumber faxNumber;     // ditto
15 
16 };            

 

在这个例子中,Person对象由string,Address和PhoneNumber对象组成。对于程序员来说,术语组合(composition)有很多替代词。像分层(layering,包含( containment),聚合 (aggregation, 和植入(embedding)。

2. 组合关系意味着什么?

2.1 区分 “is-a”和“has-a”

Item 32中解释了public继承意味着”is-a”。组合同样有另外一个意思。事实上,有两个意思。组合即意味着“has-a”,也意味着“is-implemented-in-terms-of”。这是因为你正在处理软件中的两种不同领域(domain)。你的程序中的一些对象对应着世界上的真实存在的东西,如果你要为其建模,例如,人类,车辆,视频帧等等。这样的对象是应用域(application domain)的一部分。其他的对象则是纯实现层面的人工产品。例如,缓存(buffers),互斥信号量(mutexs),搜索树(search trees)等等。这种类型的对象对应着你软件里的实现域(implementation domain)。当组合关系发生在应用域中的对象之间时,它表示的是“has-a”关系。当发生在实现域中时,它表示的是“is-implemented-in-terms-of”关系。

上面的Person类表示的是一种“has-a”关系。一个Person对象有一个名字,一个地址和一个语音和传真电话号码。你不能说一个人“is-a”名字或者一个人“is-a”地址。你会说一个人“has-a”名字和“has-an”地址。大多数人能够很容易区分这些,因此很少有人会混淆“is-a”和“has-a”的意思。

2.2 区分 “is-a”和“is-implemented-in-terms-of”

更麻烦的是对“is-a”和“is-implemented-in-terms-of”进行区分。举个例子,假设你需要一个类模板表示很小的对象set,也即是没有重复元素的collections。因为重用是美好的事情,你的第一直觉就是使用标准库的set模板。当有一个已经被实现好的模板时你为什么要自己手动实现一个呢?

不幸的是,set的实现对于其中的每个元素都会引入三个指针的开销。因为set通常作为一个平衡搜索树来实现,这能保证将搜索,插入和删除的时间限定在对数级别(logarithmic-time)。当速度比空间重要时,这是个合理的设计,但是对于你的应用,空间比速度要更重要。所以标准库的set没有为你的应用提供正确的权衡。你需要自己实现这个模板。

 重用仍然是美好的事情。作为数据结构专家,你知道实现set会有很多选择,其中一个是使用linked lists。你同样知道标准C++库有一个list模板,所以你决定重用它。

特别情况下,你决定让你的初步实现的set模板继承list。也即是Set<T>将会继承list<T>。毕竟,在你的实现中,一个Set对象事实上是一个list对象。所以你将Set模板声明为如下:

1 template<typename T>                      // the wrong way to use list for Set
2 
3 class Set: public std::list<T> { ... };     

 

看上去都很好,但事实上有一些地方犯了严重错误。正如Item32中解释的,如果D 是一个B,对于B来说是真的对D来说也是真的。然而,一个list对象可能会包含重复元素,所以如果值3051被插入到Set<int>两次,那么list将会包含3051的两个拷贝。相反。一个Set不可以包含重复元素,所以当3051被插入到Set<int>两次的时候,set只包含一个3051值。现在一个Set是一个List就不再为真了,因为对list对象为真的一些事情对Set对象来说不为真了。

因为这两个类之间的关系不是“is-a”,public继承是为这种关系建模的错误方式。正确的方式是意识到一个Set对象可以被“implemented in terms of”一个list对象:

 

 1 template<class T>                           // the right way to use list for Set
 2 
 3 class Set {                                       
 4 
 5 public:                                            
 6 
 7 bool member(const T& item) const; 
 8 
 9 void insert(const T& item);            
10 
11 void remove(const T& item);         
12 
13 std::size_t size() const;                   
14 
15 private:                                          
16 
17  
18 
19 std::list<T> rep;              // representation for Set data
20 
21 };                 

             

 Set成员函数的实现可以依赖list已经提供的功能和标准库的其他部分,所以实现上就简单直接了,前提是你对STL编程的基本知识很熟悉:

 1 template<typename T>
 2 bool Set<T>::member(const T& item) const
 3 {
 4 return std::find(rep.begin(), rep.end(), item) != rep.end();
 5 }
 6 template<typename T>
 7 void Set<T>::insert(const T& item)
 8 {
 9 if (!member(item)) rep.push_back(item);
10 }
11 template<typename T>
12 void Set<T>::remove(const T& item)
13 {
14 typename std::list<T>::iterator it = // see Item 42 for info on
15 std::find(rep.begin(), rep.end(), item); // “typename” here
16 if (it != rep.end()) rep.erase(it);
17 }
18 template<typename T>
19 std::size_t Set<T>::size() const
20 {
21 return rep.size();
22 }

 

这些函数足够简单,它们是inline函数的合理候选人,虽然我知道你想先回顾一下Item30中的讨论之后再做决定。

一些人会争论为了让Set接口更好的符合Item18的建议,也即是设计接口的时候满足容易被正确使用不容易被误用,Set应该遵循STL容器的惯例。但是遵循这些惯例就需要为Set增加很多工作,这会导致list和Set之间的关系模糊不清。既然关系是这个条款的关键点,我们会为了更好的阐述而牺牲STL兼容性。此外,Set接口不应该使Set的无可争辩的正确行为黯然失色:这个权利是它和List之间的关系。这个关系不是”is-a”(虽然一开始看起来像),而是”is-implemented-in-terms-of”。

3. 总结

 

    • 组合和public继承的意义完全不同
    • 在应用域中,组合意味了“has-a”,在实现域中,它意味着“is-implemented-in-terms-of”
posted @ 2017-03-27 20:36  HarlanC  阅读(953)  评论(0编辑  收藏  举报