以下template提供了list的管理功能,包括在特定位置上的处理元素的功能:
//Example 1
template<class T>
class Mylist
{
public:
bool Insert(const T&,size_t index);
T Access(size_t index) const;
size_t Size() const;
private:
T* buf;
size_t bufsize_;
};
考虑如下的代码,其中实现以MyList为基础,实现一个MySet class的不同做法。假设所有的重要元素都已实现:
//例1(a)
template<class T>
class MySet1 : private MyList<T>
{
public:
bool Add(const T&); //call insert
T Get(size_t index) const; //call Access
using MyList<T>::Size;
//...
};
//例1(b)
template <class T>
class MySet2
{
public:
bool Add(const T&); //call impl_.insert
T Get(size_t index) const; //call impl_.Access
size_t Size() const; //call impl_.Size
//...
private:
MyList<T> impl_; //containment
};
请思考如下的问题:
- MySet1和MySet2之间的任何差异?
- 广泛的说nopublic inheritance 和containment有什么不同?说明你为什么采用inheritance而不采用containment。
- 你比较喜欢哪一个版本?
- 尽可能的说明你为什么喜欢public inheritance。
【解答】
1.答案:两者之间不具有实际意义的差别,它们的机能完全相同。
2.答案:
- Nonpublic inheritance应该总是表现根据某物实现(IS-IMPLEMENT-IN-TERMS-OF)的意义,它使用using 只能依赖class 的public/protected部分。
- Containment总是表现HAS-A的意义,连带有根据某物实现(IS-IMPLEMENT-IN-TERMS-OF)的意义。它使用using containment只能依赖class的public部分。
容易看出,inheritance是single containment的一个超集,也就是说,没有什么可以用一个MyList<T> member实现的而却无法以继承自MyList<T>完成的事。当然了,inheritance造成只能拥有一份MyList<T>(作为base subobject):如果我们想拥有多份MyList<T>实体,就必须改用containment。
设计准则:尽量采用aggregation(又名“composition”,“layering”,“HAS-A”,“delegation”)来取代inheritance。当我们模塑IS-IMPLEMENTED-IN-TERMS-OF关系时,尽量采用aggregation而不要使用inheritance。
如下是使用nopublic inheritance的理由:
- 我们需要改写一个虚函数。
- 当我们需要处理protected member。
- 当我们需要在一个base subobject之前构建used object。
- 当我们需要分享某种共享的virtual base class或改写virtual base class的构造程序。
- 当我们从empty base class的最佳化获益时。
class B{/*只有函数,没有数据*/};
//包含:有空间开销
class D
{
B b_; //b_必须占一个字节
//即使B是空类
};
class D : private B
{
//B子对象无须占用任何内存
};
- 当我们需要“受控的多态性(controlled polymorphism)”——LSP IS-A。public inheritance总是模塑IS-A的关系,nonpublic inheritance可以表现受限制的IS-A关系。虽然大部分人都是以public inheritance来识别IS-A。
3.答案:
- Mylist没有保护成员,我们不需要访问它们。
- Mylist没有虚函数,我们不需要继承它们并改写它们。
- Myset没有其他潜在的base classes。所以Mylist不需要再另一个base subobject之前构建和之后析构。
- Mylist没有任何的virtual base classes是Myset可需要共享的,或construction可能需要改写的。
- Mylist不是空类,所以empty base class最优化动机并不适用。
- Myset不是一个(IS-NO-A)Mylist,甚至不再Myset的成员函数和友元函数中。
简单的来说,Myset不应该继承Mylist。在containment同样有效的情况下使用inheritance,只会引入无偿的耦合性和不必要的依赖性。
如果要改写一个虚函数如Func1或存取一个protected member如Func2,就必须采用inheritance不可,但是下面的做法是不需要的吗?
//例2(a)
class B
{
public:
virtual int Func1();
protected:
bool Func2();
private:
bool Func3(); //使用Func1
};
class Derived : private Base
{
public:
int Func1();
//更多函数,其中一些使用了Base::Func2
};
这段代码允许Derived改写Base::Func1,这很好。不幸的是,它也允许让所有的Derived members使用Base::Func2,这就是问题所在。通过这样使用继承,我们所有的Derived的成员都依赖于Base的保护接口,这大可不必要。
下面的做法更加好:
//2(b)
class DerivedImpl : private Base
{
public:
int Func1();
//...这里的函数需要使用Func2
};
class Derived
{
//...这里的函数不需要使用Func2
private:
DerivedImpl* impl_;
};
这个设计好多了,因为它很好的隔离和封装了对于Base的依赖性。Derived只能直接依赖Base的共有接口和DerivedImpl的公有接口。设计成员的原因:遵循一个类,一个职责的设计原则。
现在看看containment的优点:第一,它允许我们永远多个used class的实例,这对inheritance而言不可能。
如果你继续派生有需要多份实例,请使用2(b)所展示的方法:派生出一个辅助类(DerivedImpl)负责继承的工作,然后再再使用这个辅助类的多个实例。
第二,它令used class成为一个data member,这样带来更大的弹性。那个member可以被隐藏在Pimpl内部的编译器防火墙之后,而且有必要在运行时改变的话,可以轻易的被转换成指针(尽管继承体系是静态并在编译器就固定了)。
最后更加1(b)重写Myset2的第三种有效方法,它以一种更普遍的方式来使用包含:
//例1(c),泛型包含
template <class T,class Impl = Mylist<T> >
class MySet3
{
public:
bool Add(const T&); //调用 impl_.Insert();
T Get(size_t index);//调用impl_Access();
size_t Size() const;//调用impl_Size();
//...
private:
Impl impl_;
};
现在我们有了更加的弹性,不再只是以Mylist<T>来实现,更可以令Myset根据任何class为基础来实现——只要它们支持Add,Get以及其他需要的函数。
4.答案:
public inheritance用来模塑真正的IS-A关系,一如里氏替换原则所说:子类即基类,只要可以使用base class object的地方,就应该可以使用publicly derived class object。
遵循以下规则可以避免常见的一些易犯的错误:
- 如果nopublic inheritance能用的情况下,就绝对不要考虑public inheritance。
- 如果class相互关系可以以多种方式来表现,请使用最弱的一种关系。
不要被诱惑:如果其行为不像基类,它就不是一个Base,那就不要以(使它看起来像个Base)的方式来派生它。
设计准则:总是确定public inheritance用来模塑IS-A和WORK-LIKE-A的关系,并符合里氏替换原则。所有被改写的member function都必须不要求更多,也不承诺更少。
设计准则:绝对不要为了重用base class中的代码而使用public inheritance。public inheritance的目的是为了多态方式重用base object。
结论:
明智的运用inheritance。如果你单独以containment/delegation表现某个class相互关系,你就应该这么做。如果你需要inheritance但不想模塑出IS-A关系,请使用nopublic inheritance。如果你不需要多重继承的威力,请使用单一继承。一般而言,大而深的继承体系特别难理解,因此也就难以维护。inheritance是一个设计期决定,必须舍去许多运行期的弹性。
一些人以为,除非使用继承体系,某则算不上面向对象设计,这实际上是不对的。尽可能使用可有效的解答中最简单的一个,那么你还可能多享受几年稳定而容易维护的代码。