Java教程

使用经验 91 区分继承、模版还有组合

本文主要是介绍使用经验 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 区分继承、模版还有组合的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!