继承与面向对象设计
确定你的public继承塑膜出is-a关系
“public继承”意味”is-a”。适用于基类身上的每一件事情也一定适用于派生类身上。因为每一个派生类对象也都是一个基类对象。
公开继承意味着”is-a”关系,private继承的意义与此完全不同,至于protected继承,其意义至今令人困惑。
“is-a”并非是唯一存在于类之间的关系。另两个常见关系是”has-a”和”is-implemented-in-terms-of”。
避免屏蔽继承而来的名称
派生类内的名称会屏蔽基类中的名称(就像局部变量名屏蔽全局变量名)。在public继承下从来没有人希望如此。
为了让被屏蔽的名称重见天日,可以使用using
声明式或转交函数。
- using声明
class Base{
public:
void Foo();
}
class Derived : public Base{
public:
using Base::Foo;
}
- 转交函数
class Base{
public:
virtual void Foo() = 0;
}
class DerivedClass : public BaseClass{
public:
virtual void Foo(){ Base::Foo(); }//转交函数
}
区分接口继承和实现继承
接口继承与实现继承不同。在public继承之下派生类总是继承基类的接口。
纯虚函数只具体指定接口继承。
非纯虚函数具体指定接口继承以及default实现继承。
非虚函数具体指定接口继承以及强制性实现继承。
考虑virtual函数以外的其他选择
跳脱面向对象设计路上的常轨,考虑其他的一些解法。
virtual函数的替代方案包括NVI手法以及策略模式的多种形式。NVI自身是一个特殊形式的模板方法设计模式。
- 籍由Non-Virtual-Interface(NVI)手法实现模板方法设计模式
class GameCharacter
{
//...
public:
int healthValue() const
{
DoSomethingFirst();
int ret = health();
DoSomethingThen();
return ret;
}
private:
virtual int health() const{};
}
NVI手法的一个优点是可以“做一些事前工作”和“做一些事后工作”。这意味着包装器(wrapper)确保得以在一个虚函数被调用之前设定好适当场景,并在调用结束后清理场景。(例如互斥锁的锁定、日志、验证等操作)
- 籍由函数指针或者std::function实现策略模式
class GameCharacter{
public:
//typedef int (*HealthCalcFunc)(const GameCharacter&);
typedef std::function<int(const GameCharacter&)> HealthCalcFunc;
int HealthValue() const { return healthFunc(*this); }
private:
HealthCalcFunc healthFunc;
}
为了非成员函数能够访问非public成分,需要弱化类的封装。
- 古典策略模式
class HealthCalcFunc{
public:
virtual int invoke(const GameCharacter& chara) const
{...}
}
绝不重新定义继承而来的non-virtual函数
CRE:重新定义non-virtual函数违反了is-a原则。
CRE:多态基类的析构函数也应该是virtual的。
绝不重新定义继承而来的default参数值
绝对不要重新定义一个继承而来的default参数值,因为default参数值都是静态绑定的。而唯一应该覆写的东西-虚函数,却是动态绑定的。
CRE:虚函数(动态绑定)如果重写了默认参数(静态绑定),有时会导致基类函数的默认参数传入派生类的函数。
GPT:最佳实践是在派生类中不更改继承的函数的默认参数。如果需要不同的默认行为,考虑使用其他设计模式,如策略模式,或者重新设计类的接口和继承结构。
Base* obj = new Derived();
obj->Foo();//会调用Base类定义的默认参数
通过composition塑膜出has-a或者is-implemented-in-terms-of
CRE:composition在不同的领域中有不同的意义,可以是has-a或者is-implemented-in-terms-of(根据某物实现出)。
明智而审慎地使用private继承
Private继承意味is-implemented-in-terms-of(根据某物实现出)。
它通常比composition的级别低。但是当派生类需要访问基类的protected成员或者需要重新定义继承而来的虚函数时,这么设计是合理的。
和composition不同,Private继承可以造成 empty base 最优化。这对致力于“对象尺寸最小化”的程序库开发者而言可能很重要。
如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他意涵了。
Private继承在软件“设计”层面上没有意义,其意义只及于软件实现层面。
Private继承和Composition都有is-implemented-in-terms-of的意义。但是应该尽可能使用composition,必要时才使用Private继承。
必要是指牵扯到protected成员或者virtual函数的时候,或者某些情况下空间方面的利害关系的考虑。
明智而审慎地使用多重继承
多重继承比单一继承复杂。它可能导致新的歧义,以及对virtual继承的需要。
virtual继承会增加大小、速度、初始化和赋值复杂度等等成本。如果virtual基类不带任何数据,将是最具实用价值的情况。
多重继承的一个通情达理的应用:“public继承自某个接口”和“private继承自某实现”。
GPT:多重继承的应用场景:
- 接口继承。
- 功能扩展。
(END)