使用经验 91 区分继承、模版还有组合
作者:互联网
作为C++程序设计开发人员,可以考虑下面三个设计问题:
(1)设计一个描述队列类。你可能需要不同的类,因为每个队列处理的数据不同。例如,可能会员会有一个表示int的队列,同样也可能有一个表示string的队列,甚至还有表示string队列的队列等等。但是如要求你不用标准STL库,假设需设计的队列类型需具备下述操作方法:创建队列,销毁队列,将对象加入队列尾部,从头部获得对象,检查队列是否为空,那么你该如何设计呢?
(2)设计一个类型描述人。可以想象的,这也许要不同的类,甚至创建人的队列。每种类型的人都有不同的行为,例如学生要学习,工人要做工等等。但有一个共同点是:人可以创建和销毁。
(3)我们考虑人类的头部,眼、鼻、口、耳都是头的组成部分。眼可以看东西,鼻可以闻气味,口可以吃饭,耳可以听。那么你该如何设计呢?
这三个问题颇有相似之处,但却是完全不同的3种设计理念。本实用经验后续部分,将深入探索这三者的差异。
最佳实践
- 对于队列和人而言,要处理的都是各种不同的类型(队列包含类型为T的对象,人则为种类T),但你必须问自己这样一个问题:类型T影响类的行为吗?如果T不影响行为,你可以使用模板。如果T影响行为,你就需要虚函数,使用继承。
- 对于人和人头而言,要处理的是不同的行为。但是你们必须明确知道:不同的类之间的关系,如果类之间展现的是a kind of关系,那么请选择继承关系。如果所展现的是a part of关系,那么请选择组合关系。
下面是队列模板的声明实现。
// 队列模板实现。
template <class T>
class CQueue
{
//定义队列的节点结构
struct NODE
{
NODE<T>* next;
T data;
};
public:
CQueue();
virtual ~ CQueue();
//在队尾入队
void push(T e)
//在队头出队
T pop();
//判断队列是否为空
bool empty();
private:
//指向头结点的指针。 front->next->data是队头第一个元素。
NODE<T>*m_front;
//指向队尾(最后添加的一个元素)的指针
NODE<T>* m_rear;
};
队列实现了,但是人类怎么实现呢?为什么人的类实现,不适合模板呢?
前面说过,人有一个共同点:人可以创建和销毁。”。这意味着必须为每种类提供不同的行为实现。不能写一个函数来处理所有类型人的行为。我们可定制一个函数接口,让所有种类的人都实现它。这貌似就是继承啊。对的,这的确是继承。而且函数接口必须声明为一个纯虚函数。
class CPerson
{
public:
virtual ~ CPerson ();
virtual void Work() = 0;
};
人类的子类,比如Student和Worker,当然得重新定义继承而来的Work函数接口:
// 学生类实现
class CStudent: public CPerson
{
public:
virtual void Work();
};
// 工人类实现
class CWorker: public CPerson
{
public:
virtual void Work();
};
至此,我们知道了模板适合CQueue类,继承适合CPerson类。唯一剩下的问题是,为什么继承不适合CQueue类。要解决这个问题,不妨试着声明一个CQueue队列基类,所有其它的队列类,都从这个类继承:
class CQueue
{
public:
CQueue();
virtual ~ CQueue();
//在队尾入队
void push(const ??? e)
//在队头出队
T pop();
//判断队列是否为空
bool empty();
private:
//指向头结点的指针。 front->next->data是队头第一个元素。
NODE<T>* front;
//指向队尾(最后添加的一个元素)的指针
NODE<T>* rear;
};
可很明显的看到,push函数的输入参数类型无法确定,数据的类型影响函数的行为,所以这种实现方式是有问题的。
明白了继承和模板的关系,我们继续讨论继承和组合的问题。上文我们讨论过继承描述的两类的关系是is kind of。而组合描述的是a part of的关系。
组合模式实现,眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是头(Head)的一部分,所以类Head 应该由类Eye、Nose、Mouth、Ear 组合而成,不是派生而成。
// 眼睛类
class Eye
{
public:
void Look(void);
};
// 鼻子类
class Nose
{
public:
void Smell(void);
};
// 嘴类
class Mouth
{
public:
void Eat(void);
};
// 耳朵类
class Ear
{
public:
void Listen(void);
};
// 头类的实现。
class Head
{
public:
void Look(void)
{
m_eye.Look();
}
void Smell(void)
{
m_nose.Smell();
}
void Eat(void)
{
m_mouth.Eat();
}
void Listen(void)
{
m_ear.Listen();
}
private:
Eye m_eye;
Nose m_nose;
Mouth m_mouth;
Ear m_ear;
};
如果允许Head 从Eye、Nose、Mouth、Ear 派生而成,那么Head 将自动具有Look、Smell、Eat、Listen 这些功能:
class Head : public Eye, public Nose, public Mouth, public Ear
{
};
上述程序十分简短并且运行正确,但是这种设计却是错误的。Head不是Eye,不是Nose….。这不符合public继承的基本原则。虽然这儿运行正确,但是无法保证所有情况下都能正确的运行。
总结
- 当对象的类型不影响类中函数的行为时,就要使用模板来生成这样一组类。
- 当对象的类型影响类中函数的行为时,就要使用继承来得到这样一组类。
- 当对象和对象的关系是is kind of时选择继承,是a part of时选择组合。
请谨记
- 区分模板和继承的差别,当对象的类型不影响类中函数的行为时,选择使用模板,当对象的类型影响类中函数的行为时,选择继承而不选择模板。
- 区别继承和组合,如果对象关系是is kind of 关系时选择继承,如果是a part of关系时选择组合。
标签:CQueue,队列,模版,区分,public,class,91,void,继承 来源: https://blog.csdn.net/liuguang841118/article/details/120892670