***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************
六、继承与面向对象设计
six、Inheritance and Object-Oriented Design
条款 34:区分接口继承和实现继承
Rule 34:Differentiate between inheritance of interface and inheritance of implementation
首先看看有两部分组成的 public继承:
· 函数接口继承(function interfaces)
· 函数实现继承(function implementations)
它们之前的差别,就像之前的 函数声明 和 函数定义 之间的差异。
当我们设计一个 类 的时候,有时候会希望 派生类 只继承成员函数的接口(就是声明),有时候又希望派生类同时继承函数的接口和实现,但又能控制是否可以 覆写(override)所继承的实现。
1.Shape和它的继承者们
class Shape {
public:
virtual void draw() const = 0;
virtual void error( const std::string& msg);
int objectID() const;
...
};
class Rectangle : public Shape { ... };
class Ellipse : public Shape { ... };
上面例子是关于 几何形状的,Shape是一个抽象类(因为pure virtual函数 draw),
Rectangle 和 Ellipse是Shape的派生类,它们都是 public 继承~
Shape类,定义了三个函数
· draw函数,作用就是在某个地方画出当前对象,pure virtual函数
· error函数,当需要报错时候调用,impure virtual函数
· objectID函数,返回当前对象的标志,nont-virtual函数
2.Shape所拥有的三个遗产
> virtual void draw() const = 0;
pure virtual函数有两个特性:必须被任何继承它们的具象类重新声明,而且在抽象类中通常没定义。
这两个特性,使得 pure virtual函数可以做到——让派生类只继承函数接口
draw的作用:每个Shape都应该可以被画出来,但是基类没办法具体提供一个绘画的版本(毕竟画个圆和画个矩形差别还是很大的),所以每个派生类都应该提供一个draw函数,但基类不管你如何实现。
但如何调用呢?唯一途径就是 调用时,明确指出它的class的名称:
Shape* ps = new Shape; // error!Shape是抽象类,不能创建实体
Shape* ps1 = new Rectangle; // 没问题
ps1->draw(); // 调用Rectangle的draw函数
Shape* ps2 = new Ellipse; // 没问题
ps2->draw(); // 调用Ellipse的draw函数
// 调用 Shape的draw函数
ps1->Shape::draw();
ps2->Shape::draw();
> virtual void error(const std::string& msg);
impure virtual函数可以做到——让派生类继承函数接口,但它会提供一份实现代码,派生类也可以覆写(override)它。
error作用:告诉所有 继承Shape类的派生类,你必须支持一个 error函数,可以自己重写一个,也可以用Shape基类提供的缺省版本。
☆ But 允许 impure virtual函数同时指定函数声明与函数缺省行为,会有些危险,具体可以先看下面这个例子
例:XYZ航空公司设计的飞机继承体系,有A型与B型两种飞机,两者都以相同方式飞行
class Airport { ... }; // 机场
class Airplane {
public:
virtual void fly(const Airport& destination);
...
};
void Airplane::fly(const Airport& destination)
{
// 缺省代码,将飞机飞至指定目的地
}
class ModelA : public Airplane { ... };
class ModelB : public Airplane { ... };
这是一份典型的面向对象设计,两个class共享一份相同的性质(fly),共同的性质放到基类中,然后被这两个不同派生类继承。
这样可以避免代码重复,让代码清晰,减少维护成本等~~
目前为止,没问题。
现在第一次维护,XYZ公司决定用一种新式飞机来开辟一条新的航线,但因为项目时间太紧,只来得及添加一个class,而忘记覆写这个航线(fly函数)。
class ModelC : public Airplane {
...
};
这样,显然会出现问题——C型飞机航线是不同于A与B的,我们如果让C型飞机 fly,它会走A与B的航线(缺省行为)。
问题不在于Airplane::fly有缺省行为,而是在于C型飞机在被调用fly时,并没有想调用缺省行为,却被迫调用。
所以,我们来解决这个问题。
第二次维护,我们让派生类决定,它是否要用这个函数的缺省行为
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
};
void defaultFly(const Airport& destination)
{
// 缺省行为,将飞机飞向指定目的地
}
这时候,Airplane::fly函数已经是 pure virtual函数了,只继承了接口,但缺省行为也在基类中,需要派生类决定是否去调用它(通过inline调用)
class ModelA : public Airplane {
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
};
class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
};
这样当定义了C型飞机后,你就不得不让C型飞机提供一份 fly函数版本,给出一份航线。
如果让你给航线了,你还去专门走A、B型飞机的航线,那我也无奈了....
另外,再有一点,有人反对用不同的函数分别提供接口和缺省实现,她们认为,你接口和实现应该用同一个名字,实现起来也不麻烦,就是利用“pure virtual”函数,必须在派生类中重新声明,但它们也可以有自己的实现 这个特性
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
};
void Airplane::fly(const Airport& destination) // pure virtual的函数实现
{
// 缺省行为,将飞机飞向指定目的地
}
class ModelA : public Airplane {
public:
virtual void fly(const Airport& destination)
{ Airplane::fly(destination); }
...
};
class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination)
{ Airplane::fly(destination); }
...
};
class ModelC: public Airplane {
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination)
{
// 指定C型飞机的航线
}
这个几乎和前面设计一模一样,只不过用 pure virtual函数fly替换了之前的独立函数 defaultFly。
但这样做,就失去了让两个函数有不同的保护级别的机会,后者 缺省实现是 public,前者 缺省实现是protected。
> int objectID() const;
non-virtual函数可以做到——让派生类继承函数的接口及一份强制性实现(就是不能再派生类被覆写)
objectID作用:每个Shape对象都有一个用来识别对象的函数,这个识别码总会用同样的方法去产生,该方法由基类的函数(Shape::objectID)决定,派生类不能去改变它。
我们可以看到,将函数声明为不同形式——pure virtual、impure virtual、non-virtual,
它可以实现不同的目的——让派生类 只继承接口、继承接口和一份缺省的实现、继承接口和一份强制的实现。
3. 继承者们面对遗产的要注意两个原则
① 我们要避免将所有函数都声明为 non-virtual
因为这样会让 派生类 没有足够空间去展现它们的特点,尤其是non-virtual的析构函数。
如果不想让这个类成为 基类(base class),是否就可以都设计成non-virtual函数呢?
当然不!
实际上,这如果不是对 virtual 与 non-virtual 之间差异不了解,就是太担心virtual函数的效率成本。
但 80-20法则 告诉我们,平均来说函数调用中可以有80%是virtual而不冲击程序的大体效率,因为一个典型程序的80%执行时间花费在20%的代码上;所以在担心 virtual函数 效率成本之前,应该先去管管那20%的代码。
② 同时要避免所有的函数都声明为 virtual
有时候,这样是没有错误的——比如,前面介绍 interface class(接口类)的时候< 详见 条款31 >
但,某些函数就不该在派生类被修改,就比如 Shape生成ID,如果每个同样的Shape,一个矩形,一个椭圆形,它们ID生成方法不同,就可能导致ID号相同,这显然是不正确的。
4.请记住
> 接口继承和实现继承不同。在public继承下,派生类总是继承基类的接口
> pure virtual函数只具体指定接口继承
> impure virtual函数具体指定接口继承与一份缺省实现继承
> non-virtual函数具体指定接口继承以及一份强制性实现继承
***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************