Effective C++ 条款34、35

条款34 区分接口继承和实现继承

函数接口继承(类比声明)和函数实现继承(类比定义)。

先见如下代码举例,

class Shape {
public:
	virtual void draw() const = 0;//纯虚函数作为函数接口被继承
	virtual void error(const string& msg);//impure virtual,上让derived classes继承函数的接口和缺省实现
	int objectID()const;//non-virtual函数,绝对不要在derived class类中重新定义
};

class Rectangle::public Shape{};
class Ellipse::public Shape{};

于是有以下结论,

i、成员函数的接口总是会被继承

ii、声明一个pure virtual函数的目的是为了让derived class只继承函数接口

(有必要说明下,在C++中是可以为pure virtual函数提供定义的,但调用它的唯一途径是“调用时明确指出其class名称”)

iii、声明impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现;(也就是说,让derived class对virtual函数进行特化过程)

针对第三点,举一实例:某机场两种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{};
如果此时,新增一架新型号飞机,但忘了重新定义fly函数,此时fly将按A、B型号的飞机飞往目的地。
class ModelC:public Airplane{};
//然后代码中有一些诸如此类的动作
Airport PDX();//作者家附近的机场
Airplane* pa = new ModelC;

pa->fly(PDX);//于是将按A、B型号的飞机飞行
避免上述问题的做法是,切断“virtual函数接口”和其“缺省实现”之前的连接。见如下代码改写,
class Airplane {
public:
	virtual void fly(const Airport& destination) = 0;
protected:
	void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination) {
	/*缺省行为,将飞机飞到指定的目的地*/
}

//于是在派生类中重新改写fly函数
class ModelA :public Airplane {
public:
	virtual void fly(const Airport& destination) {
		defaultFly(destination);
	}
};
class ModelB :public Airplane {
	virtual void fly(const Airport& destination) {
		defaultFly(destination);
	}
};

class ModelC :public Airplane {
	virtual void fly(const Airport& destination);
};
void ModelC::fly(const Airport& destination) {
	//将C开型飞机飞至指定的目的地
}

iv、声明non-virtual函数的目的是为了令derived classes继承函数的接口以及一份强制性实现。(详情见条款36)

条款35 考虑virtual函数以外的其他选择

i、借由Non-Virtual Interface(NVI)手法实现Template Method模式

令客户通过public non-virtual成员函数间接调用private virtual函数,此处non-virtual函数称为virtual函数的外覆器。外覆器的好处就是确保得以在一个virtual函数被调用之前设定好适当场景,并在结束之后清理现场。如下代码举例,

class GameCharacter {
public:
	int healthValue()const {
		...//做一些事前工作
		int retVal = doHealthValue();//做真正的工作
		...//做一些事后工作
	}
private:
	virtual int doHealthValue()const {
		...//缺省算法以,计算健康指数
	}
};

ii、借由Function Pointers实现Strategy模式

此设计手法的好处一,同一人物类型之不同实体可以有不同的健康计算函数,如下代码举例,

class GameCharacter {
public:
	int healthValue()const {
		...//做一些事前工作
		int retVal = doHealthValue();//做真正的工作
		...//做一些事后工作
	}
private:
	virtual int doHealthValue()const {
		...//缺省算法以,计算健康指数
	}
};
class EvilBadGuy :public GameCharacter {
public:
	explicit EvilBadGuy(HealthCalcFunc hcf=defaultHealthCalc):
		GameCharacter(hcf){}
};
int loseHealthQuickly(const GameCharacter&);//健康指数计算函数1
int loseHealthSlowly(const GameCharacter&);//健康指数计算函数2

EvilBadGuy ebg1(loseHealthQuickly);//相同类型的人物搭配不同的健康计算方式
EvilBadGuy ebg2(loseHealthSlowly);
好处二,某已知人物的健康指数计算函数可在运行期变更。例好GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。(说实话,这点我是没有看懂的!)

iii、借由function完成Strategy模式

第ii点基于函数指针的做法看起来有点苛刻死板,于是我们可以改用一个类型为function的对象。这样的对象可持有(保存)任何可调物(如函数指针、函数对象或成员函数指针)。举例代码如下,

class GameCharacter;//前置声明
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
	/*HealthCalcFunc可以是任何“可调用物”,可被调用并接受任何兼容于GameCharacter的物,
	返回任何兼容于int的东西*/
	typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
	explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) :
		healthFunc(hcf) {}
	int healthValue()const {
		return healthFunc(*this);
	}
private:
	HealthCalcFunc healthFunc;//此处声明healthFunc函数
};
上述代码中, function<int (const GameCharacter&)>中的签名式为int (const GameCharacter&)表示接受一个reference指向const GameCharacter,并返回int。此function类型(也就是所定义的HealthCalcFunc类型)产生的对象可以持有(保存)任何与此签名式兼容的可调用物。所谓兼容,即是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int。具体用法如下举例,
short calcHealth(const GameCharacter&);
struct HealthCalculator {
	int operator()(const GameCharacter&)const{}
};
class GameLevel {
public:
	float health(const GameCharacter&)const;
};
class EvilBadGuy:public GameCharacter{};
class EyeCandyCharacter:public GameCharacter{};

EvilBadGuy ebg1(calcHealth);//人物1使用某个函数计算健康指数

EyeCandyCharacter ecc1(HealthCalculator());//人物2,使用某个函数对象计算健康指数

GameLevel currentLevel;
EvilBadGuy ebg2(bind(&GameLevel::health, currentLevel, _1));//人物3,使用某个成员函数计算健康指数
//最后一条代码就是温故bind函数的用法,其中currentLevel类似隐匿传参,每次指出ebg2的健康计算函数应该总是以currentLevel作为Gamelevel对象,
//_1为占位符,1表示占据了第1个参数。

iv、古典的Strategy模式

传统的Strategy做法将健康计算函数做成一个分离的继承体系中的virtual成员函数。


上图中,每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象。具体代码实现如下,

class GameCharacter;//前置声明

class HealthCalcFunc {
	virtual int calc(const GameCharacter& gc)const{}
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
	typedef int(*HealthCalcFunc)(const GameCharacter&);
	explicit GameCharacter(HealthCalcFunc*  phcf = &defaultHealthCalc) :
		pHealthFunc(phcf) {}
	int healthValue()const {
		return pHealthCalc->calc(*this);
	}
private:
	HealthCalcFunc* pHealthFunc;//此处声明healthFunc函数
};

综上,

i、使用NVI手法,那是Template Method设计模式的一种特殊形式,它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数。

ii、将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。

iii、以function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。

iv、将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。

v、还有其它virtual函数的替换方案哈。

以上四点设计,只想说明,《设计模式》一书的内容还是非常重要的,否则很难看懂哈!

以上内容均来自Scott Meyers大师所著Effective C++ version3,如有错误地方,欢迎指正!相互学习,促进!!

猜你喜欢

转载自blog.csdn.net/TT_love9527/article/details/80706413
今日推荐