作为C++程序设计开发人员,可以考虑下面三个设计问题:
(1)设计一个描述队列类。你可能需要不同的类,因为每个队列处理的数据不同。例如,可能会员会有一个表示int的队列,同样也可能有一个表示string的队列,甚至还有表示string队列的队列等等。但是如要求你不用标准STL库,假设需设计的队列类型需具备下述操作方法:创建队列,销毁队列,将对象加入队列尾部,从头部获得对象,检查队列是否为空,那么你该如何设计呢?
(2)设计一个类型描述人。可以想象的,这也许要不同的类,甚至创建人的队列。每种类型的人都有不同的行为,例如学生要学习,工人要做工等等。但有一个共同点是:人可以创建和销毁。
(3)我们考虑人类的头部,眼、鼻、口、耳都是头的组成部分。眼可以看东西,鼻可以闻气味,口可以吃饭,耳可以听。那么你该如何设计呢?
这三个问题颇有相似之处,但却是完全不同的3种设计理念。本实用经验后续部分,将深入探索这三者的差异。
下面是队列模板的声明实现。
// 队列模板实现。 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继承的基本原则。虽然这儿运行正确,但是无法保证所有情况下都能正确的运行。