33 将非尾端类(non-leaf classes)设计为抽象类(abstract classes)

假设你正在进行一个项目,用软件来处理动物。只有两种动物:蜥蜴和鸡,如下是组织形式:

Animal class 将负责你所处理的所有动物的共同特征具现化,Lizard和Chicken class则分别将Animal特殊化为蜥蜴和鸡:

class Animal
{
public:
	Animal& operator = (const Animal& rhs);
	...
};

class Lizard: public Animal
{
public:
	Lizard& operator = (const Lizard& rhs);
	...
};

class Chicken: public Animal
{
	Chicken& operator = (const Chicken& rhs);
	...
};

 考虑如下的代码:

Lizard liz1;
Lizard liz2;

Animal* pAnimal1 = &liz1;
Animal* pAnimal2 = &liz2;
...
*pAnimal1 = *pAnimal2;

上述代码有两个问题。第一,最后一行调用了Animal class的赋值操作,即使设计的对象是Lizard。导致部分赋值——只有“liz1的Animal成分”被修改。第二,通过指针对对象进行赋值,我们希望赋值动作的行为更加合理些。

一个解决的方法就是让assignment操作符成为虚函数,上述赋值动作就会正确:

class Animal
{
public:
	virtual Animal& operator = (const Animal& rhs);
	...
};

class Lizard: public Animal
{
public:
	virtual Lizard& operator = (const Animal& rhs);
	...
};

class Chicken: public Animal
{
	virtual Chicken& operator = (const Animal& rhs);
	...
};

但是这一规则强迫我们在每一个class中为此虚函数声明完全一样的参数类型。那就意味着Lizard和Chicken classes的assignment操作符必须接受“任何类型的Animal出现在赋值动作的右边”,导致必须面对一个事实——以下代码合法:

Lizard liz;
Chicken chick;

Animal* pAnimal1 = &liz;
Animal* pAnimal2 = &chick;
...
*pAnimal1 = *pAnimal2;	//将一只鸡赋值给一只蜥蜴

我们希望同类型的可以赋值,异型的不能赋值。要区分这些必须在运行期才有办法,我们可以利用dynamic_cast协助完成任务,下面是Lizard的assignment操作符的做法:

Lizard& Lizard::operator = (const Animal& rhs)
{
	const Lizard& rhs_liz = dynamic_cast<const Animal&>(rhs);
	
	proceed with a normal assignment of rhs_liz to *this;
}

但是上述函数在平常情况下(将一个Lizard对象赋值给另一个Lizard对象)就没必要如此复杂和昂贵的代价。如果动用dynamic_cast就会动用一个type_info结构体。我们不需要dynamic_cast的复杂度和成本,我们依然可以处理这种情况,做法就是Lizard加上传统的assignment操作符(形成两个重载函数):

class Lizard: public Animal
{
public:
	virtual Lizard& operator = (const Animal& rhs);
	
	Lizard& operator = (const Lizard& rhs); //加上这行
};

Lizard liz1,liz2;
...
liz1 = liz2l 	//调用的const Lizard& 的operator=

Animal* pAnimal1 = &liz1;
Animal* pAnimal2 = &liz2;
...
*pAnimal1 = *pAnimal2;//调用的是const Animal&的operator=

事实上,有了第二个operator=之后,第一个operator可以简化:

Lizard& Lizard::operator = (const Animal& rhs)
{
	return operator = dynamic_cast<const Animal&>(rhs);
}

这次我们将rhs转型为Lizard,如果成功就调用正常的operator操作符,否则抛异常。还有另外一种方法就是阻住clients一开始就做出有问题的赋值动作,让这种的动作在编译器就被拒绝,让operator=成为Animal的private函数:

Lizard liz1,liz2;
...
liz1 = liz2;		//正确

Chicken chick1,chick2;
...
chick1 = chick2;	//正确

Animal* pAnimal1 = &liz1;
Animal* pAnimal2 = &chick1;
...
*pAnimal1 = *pAnimal2; //错误,试图调用private
						//Animal::operator=

不幸的是,Animal作为一个具体类,而上述却使得Anial独享彼此间的赋值动作也不合法:

Animal animal1,animal2;
...
animal1 = animal2;	//错误,试图调用private
						//Animal::operator=

此外,它也造成我们无法正确实现Lizard和Chicken的assignment操作符,因为derived的assignment操作符有义务调用base classes的assignment操作符:

Lizard& Lizard::operator = (const Lizard& rhs);
{
	if(this == &rhs) return *this;
	Animal::operator=(rhs); //错误,试图调用private
							//Animal::operator=
	...
}

将Animal::operator=声明为protected可以解决后一个问题,但前一个问题存在,阻止Lizard和Chicken对象之间部分赋值(通过Animal指针)的同时,我们必须允许Animal对象相互赋值。

解决的方法ji㐊以一个新的class为抽象类,然后再令每个具体类继承自抽象类,修改后的继承体系如下:

于是各个classes定义如下:

class AbstructAnimal
{
protected:
	AbstructAnimal& operator=(const AbstructAnimal& rhs);
	...

public:
	virtual ~AbstructAnimal() = 0;
};

class Animal: public AbstructAnimal
{
public:
	Animal& operator = (const Animal& rhs);
	...
};

class Lizard: public AbstructAnimal
{
public:
	Lizard& operator = (const Lizard& rhs);
	...
};

class Chicken: public AbstructAnimal
{
public:
	Chicken& operator = (const Chicken& rhs);
	...
};

 这个设计提供你需要的每一样东西。蜥蜴、鸡、动物之间允许同类型的赋值;部分赋值和异型赋值都在被禁止之列;derived的assignment操作符可以调用base的assignment操作符。

如果你有两个具体类C1和C2,而你希望C2以public方式继承自C1.你应该将原本的双类继承体系改成三类继承体系:产生一个抽象类A,并令C1和C2都以public继承A。

 .

当你使用第三方厂商的各种C++类库的时候,如果你发现自己需要产生一个具体类,继承自成都库的一个具体类,而你zh能使用该程序库,不能修改,怎么办?下面是方法:

  • 将你的具体类派生自既有的(程序库的)具体类,但需要注意本条款一开始所验证的assignment相关问题,还有多态数组的问题。
  • 试着在程序库集成体系中找到更高的抽象类,其中有你需要的大部分功能,继承它。
  • 以“你所希望继承的那个程序库类”来实现你的新类,相当于包含程序库的对象,has-a的关系。
  • 做个乖宝宝,手上有什么就用什么。

准则是:集成体系中的non-leaf(非尾端)类应该是抽象类。如果你使用外界供应的程序库,你或许可以对此法则做点变通;但如果代码完全在你的掌控之下,坚持这个法则,可以为你带来许多的好处,并提升整个软件的可靠度、健壮度、精巧度、扩充度。

猜你喜欢

转载自blog.csdn.net/weixin_28712713/article/details/82530764