通过创建一个类来支持新类型的灵活创建,其每个实例都代表一个不同的对象类型。
(摘自《游戏编程模式》)
有两种实现多态的方式,“Have One”和继承。而类型对象模式就是运用“Have One”。类型对象模式的处理和享元模式类似,都是需要将一类对象的共有部分独立成一个类并采用“Have One”将其一一对应起来。两者的区别主要在于:类型对象模式独享一个实例;而享元模式是通过指针共享一个相同实例。
事实上,类型对象模式的主要目的就是简化继承树的规模。假设我们开发的游戏用100种怪物,每一种怪物都有一个共有的数据部分,如果我们使用继承的方式,将公共数据编写成一个基类Monster,那么我们就需要编写100种怪物类继承Monster!
class Monster{}
class Monster1 : Monster{...}
class Monster2 : Monster{...}
class Monster3 : Monster{...}
class Monster4 : Monster{...}
class Monster5 : Monster{...}
//...
类型对象模式的代码会将代码组织为如下所示,在类中就没有了继承关系。除此之外,在将怪物共有数据编写成持久化配置文件的时候,我们仅仅需要考虑Monster的序列化即可,如果采用继承将会十分麻烦——我们还需要判断我们要反序列化的Monster数据到底属于的是哪一个子类。
class Monster{}
class Monster1{private:Monster& monster;}
class Monster2{private:Monster& monster;}
class Monster3{private:Monster& monster;}
class Monster4{private:Monster& monster;}
class Monster5{private:Monster& monster;}
//...
类型对象模式
使用情景
- 同一性质对象的不同种类众多
- 这些种类是经常需要变动的(增加或删除)
- 将类型从硬编码中解放出来(不需要编译代码便可以添加或删除类型)
相较于使用继承多出的麻烦事
-
使用继承有编译器自动出来的虚函数机制来完成多态;而对于类型对象模式需要手动管理内存中存在的实例以及管理他们的类型。
-
为每个子类型定义行为将更加困难。因为类型判断主要依靠的是类中拥有的类型对象。而针对不同类型编写不同的行为代码将会全部编写在同一个class中。看下面代码会更能理解:
//继承方式 class Monster { public: virtual void Search()=0; } class Monster1 : Monster { public: virtual void Search() { AISystem.Search.Way(1); } } class Monster2 : Monster { public: virtual void Search() { AISystem.Search.Way(2); } } //类型对象模式 class MonsterCommon { //... } public Monster { public: MonsterCommon& monster; void Search() { //如何从monster中判断类型并执行不同的寻路方式? AISystem.Search.Way(1); } }
字节码模式或解释器模式就很好的将行为从代码中分离 出来,是这一问题的一个更彻底、更高级的解决方案。
示例
我们以多个怪物的案例来展示类型对象的代码组织。在一个怪物中,有不同的血量和攻击数值字符串
基础
//怪物种类
class MonsterBreed
{
public:
MonsterBreed(double _hp,const char* _attack) : hp(_hp),attack_str(_attack){}
int GetHeath(){return hp;}
const char* GetAttack(){return attack_str;}
private:
double hp;
char* attack_str;
}
class Monster
{
public:
Monster(MonsterBreed& _breed) : hp(_breed.GetHealth()),breed(_breed){}
const char* GetAttack()
{
return breed.GetAttack();
}
private:
double hp;
MonsterBreed& breed;
}
让MonsterBreed更像类型
如何让MonsterBreed更像一个类型呢?类型的作用就是我们可以通过类型实例化一切示例。那么,可以把实例化的功能拥有权给予MonsterBreed类即可。注意的是,我们要把之前提供给Monster的方法隐藏,并通过友元类将他们联系起来。
class MonsterBreed
{
public:
MonsterBreed(double _hp,const char* _attack) : hp(_hp),attack_str(_attack){}
Monster* NewMonster()
{
return new Monster(*this);
}
private:
int GetHeath(){return hp;}
const char* GetAttack(){return attack_str;}
double hp;
char* attack_str;
}
class Monster
{
friend class MonsterBreed;
public:
Monster(MonsterBreed& _breed) : hp(_breed.GetHealth()),breed(_breed){}
const char* GetAttack()
{
return breed.GetAttack();
}
private:
double hp;
MonsterBreed& breed;
}
在NewMonster()方法运用了享元模式,可以创造一类相同 的怪物。但要注意的是,类型对象的值是不能单独修改每一个怪物实例的。(详情查看享元模式的特点)。
在对象类型中实现继承
上面提到,“类型对象的值是不能单独修改每一个怪物实例”。但是,即使在同一个种类的怪物也存在差别。例如,怪物等级越高,HP可能越高。如何实现呢?我们可以自己手动实现类型对象的继承来解决这个问题。
class MonsterBreed
{
public:
MonsterBreed(MonsterBreed* _parent,double _hp,const char* _attack)
:parent(_parent),
hp(_hp),
attack_str(_attack)
{
if(parent!=nullptr)
{
if(_hp==0)
hp=parent->GetHeath();
if(_attack==nullptr)
attack_str=GetAttack();
}
}
Monster* NewMonster()
{
return new Monster(*this);
}
int GetHeath(){return hp;}
const char* GetAttack(){return attack_str;}
private:
double hp;
char* attack_str;
MonsterBreed* parent; //父类节点,也可在GetHeath()\GetAttack编写判断代码来判断是获取当前值还是“父类”的值
}
总结
类型对象与享元模式(第3章)很接近。它们都让你在实例间共享数据。享元模式倾向于节约内存,并且共享的数据可能不会以实际的“类型”呈现。类型对象模式的重点在于组织性和灵活性。