使用经验 91 区分继承、模版还有组合
2021/10/21 23:39:57
本文主要是介绍使用经验 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关系时选择组合。
这篇关于使用经验 91 区分继承、模版还有组合的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-02Java管理系统项目实战入门教程
- 2024-11-02Java监控系统项目实战教程
- 2024-11-02Java就业项目项目实战:从入门到初级工程师的必备技能
- 2024-11-02Java全端项目实战入门教程
- 2024-11-02Java全栈项目实战:从入门到初级应用
- 2024-11-02Java日志系统项目实战:初学者完全指南
- 2024-11-02Java微服务系统项目实战入门教程
- 2024-11-02Java微服务项目实战:新手入门指南
- 2024-11-02Java项目实战:新手入门教程
- 2024-11-02Java小程序项目实战:从入门到简单应用