Effective C++之条款35、36

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

    假设我们正在设计一款游戏,剧中人物被伤害而降低健康状态的情况并不罕见,因此我们要为此写一个helthValue函数,他返回一个代表健康值的整数,由于不同的人物可能以不同的方式计算他们的健康值,所以可以将healthValue声明为virtual函数:

class GameCharacter {
public:
	virtual int healthValue() const;    // 返回人物的健康指数
	...                                 // 派生类可重新定义它
};

    healthValue并未被声明为纯虚函数,这暗示我们将会有个计算健康指数的缺省算法。

    为了帮助跳脱1面向对象设计路上的常规,让我们考虑其他一些解法。

藉由Non-virtual Interface手法实现Template Method模式

    有一种流派思想是主张virtual函数应该几乎总是private。因此本设计应该保留healthValue为public成员函数,但让他成为non-virtual,并调用一个private virtual函数:

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

    这一基本设计,也就是令客户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法。它是所谓的template Method设计模式的一个独特表现形式。

    在NVI手法下其实没有必要让virtual函数一定得是private。某些class继承体系要求派生类在virtual函数的实现内必须调用其base class的对应兄弟,而为了这样的调用合法,virtual函数必须是protected,不能是private。有时候virtual函数一定得是public,这样一来就不能实施NVI手法了。

藉由Function Pointers实现strategy模式

    另一个设计主张是“人物健康指数的计算与人物类型无关”,这样的计算完全不需要“任务这个成分”。例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
	typedef int (*HealthCalcFunc) (const GameCharacter&);
	explicit GameCharater(HealthCalcFunc hcf = defaultHealthCalc)
		:healthFunc(hcf)
	{}
	int healthValue() const
	{ return healthFunc(*this);}
	...
private:
	HealthCalcFunc healthFunc;
};

    这就是常见的Strategy设计模式的简单应用。它提供了某些有趣弹性:

  • 同一人物类型之不同实体可以有不同的健康计算函数。例如:
class EviBadGuy: public GameCharacter {
public:
	explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc):
	GameCharater(hcf){...}
	...
};
int loseHealthQuickly(const GameCharacter&);  //健康值计算函数1
int loseHealthSlowly(const GameCharacter&);   //函数2

EvilBadGuy ebdg1(loseHealthQuickly);
EvilBadGuy ebd2(loseHealthSlowly);
  • 某已知人物之健康指数计算函数可在运行期间变更。例如GameCharacter可提供一个成员函数SetHeathCalculator,用来替换当前的健康值计算函数。

藉由tr1::function完成Strategy模式

    tr1::function类型地对象可持有任何可调用物,也就是函数指针、函数对象或成员函数指针,只要其签名式兼容于需求端。以下将是刚才设计改为使用tr1::function:

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharater {
public:
	typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
	explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): healthFunc(hfc){}
	int healthValue() const
	{
		return healthFunc(*this);
	}
	...
private:
	HealthCalcFunc healthFunc;
};

    和前一个设计比较,这个设计几乎相同。唯一不同的是如今GameCharacter持有一个tr1::function对象,相当于一个指向函数的泛化指针。这个改变具有更强大的弹性:在指定健康计算函数函数时,函数的返回值可以不是int型,而只要是能够隐式转换为int型即可;可指定成员函数来进行计算等。

古典的Strategy模式

    下面代码是一种标准的Strategy模式:

class GameCharacter;
class HealthCalcFunc {
public:
	...
	virtual int calc(const GameCharacter& gc) const
	{...}
	...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
	explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc): pHeathCalc(phcf) {}
	int healthValue() const
	{return pHealthCalc->calc(*this);}
	...
private:
	HealthCalcFunc* pHealthCalc;
};

请记住

  • virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
  • 将机能从成员函数移到class外部函数,带来一个缺点是,非成员函数无法访问class的non-public成员。
  • tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。

条款36:绝不重新定义继承而来的non-virtual函数

class B {
public:
	void mf();
};
class D: public {
public:
	void mf();
};
D x;
B* pB = &x;
pB->mf();     //调用B::mf
D* pD = &x;
pD->mf();    //调用D::mf

    造成此一两面行为的原因是,non-virtual函数如B::mf和D::mf都是静态绑定。这意思是,由于pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本,即使pB指向一个类型为“B派生之class”的对象。

  • 适用于B对象的每一件事,也适用于D对象,因为每个D对象都是一个B对象;
  • B的派生类一定会继承mf的接口和实现,因为mf是B的一个non-virtual函数。
发布了33 篇原创文章 · 获赞 6 · 访问量 563

猜你喜欢

转载自blog.csdn.net/weixin_43519984/article/details/102929435