理论要点
什么是类型对象:创造一个类A来允许灵活的创造新的类,而类A的每个实例都代表了不同类型的对象。通俗地讲就是定义一个类型对象类和一个有类型的对象类。每个类型对象实例代表一种不同的逻辑类型。每种有类型的对象保存描述它类型的类型对象的引用。
要点:
1,类型对象的基本思想就是给基类一个品种类(breed类),而不是用一些子类继承自这个基类。所以我们在做种类区分的时候就可以只有两个类,怪物类monster和品种类breed,而不是monster,dragon,troll等一堆类。所以在此种情况下,游戏中的每个怪物都是怪物类的一个实例,而实例中的breed类包含了所有同种类型怪物共享的信息。2,这个模式引出的进阶问题是如何在不同对象之间共享数据。以不同的方式解决同一个问题的是GOF设计模式中的原型模式。
3,类型对象是GOF设计模式中享元模式的亲兄弟。两者都让你在实例间分享代码。使用享元,意图是节约内存,而分享的数据也许不代表任何概念上对象的“类型”。而使用类型对象模式,焦点在组织性和灵活性。
使用场合:
在任何你需要定义不同“种”事物,但是语言自身的类型系统过于僵硬的时候使用该模式。尤其是下面两者之一成立时:你不知道你后面还需要什么新类型。(举个例子,如果你的游戏需要支持增量更新,让用户下载后续新包含进来的怪物品种)
修改或添加新类型不想改变代码或者重新编译。
代码分析
1,在MMO游戏中,玩家挑战副本或在野外游历都会遇到各种各样的怪物攻击,它们种类多样但都有相同属性,只是具体值不同而已。如果叫你实现这些怪物对象,你会怎么编写你的代码呢?
传统的面向对象方案一般会是这么干:
首先实现一个怪物基类:
class Monster
{
public:
virtual ~Monster() {}
virtual const char* getAttack() = 0;
protected:
Monster(int startingHealth)
: health_(startingHealth)
{}
private:
int health_; // 当前血值
};
然后,我们来实现龙和巨魔这两个品种:
class Dragon : public Monster
{
public:
Dragon() : Monster(230) {}
virtual const char* getAttack()
{
return "The dragon breathes fire!";
}
};
class Troll : public Monster
{
public:
Troll() : Monster(48) {}
virtual const char* getAttack()
{
return "The troll clubs you!";
}
};
好,传统做法已经实现了,既然是传统做法,一般都不是最好方案。想想,如果怪物种类很多,成百上千,那岂不是我要对应的上百子类。这还好,一般也不会有这么多种类,最无法忍受的是,策划如果要经常调整修改某些怪(这通常是家常便饭),如把我们巨魔的血量从48改到52,那么我们得找到troll类然后修改代码,重新编译。
没错,对于游戏程序猿们来说最坑爹的就是策划经常改需求,然后对已经写好的代码反复修改,编译。其实,被坑多了,你得学会总结经验教训,想想有么有什么办法支持策划随便调整而我能尽量少的改代码甚至不改。==>其实做法很明显了,那就是得支持读配置表。
2,下面我就来告诉你一个更高端的方案,彻底解放程序!随便想想你就能发现上面很多种类的怪其实他们属性都差不多,只是值不同而已。基于这样,我们可以把这些共同属性抽出来封装成单独的类,然后,怪物类保存一份它的引用,通过构造不同属性对象来实现不同怪物,这样都不需要继承了,就这两个类搞定。示例代码如下:
类型对象类:
class Breed
{
public:
Breed(int health, const char* attack)
: health_(health),
attack_(attack)
{}
int getHealth() { return health_; }
const char* getAttack() { return attack_; }
private:
int health_; // 初始血值
const char* attack_;
};
包含类型对象的怪物类:
class Monster
{
public:
Monster(Breed& breed)
: health_(breed.getHealth()),
breed_(breed)
{}
const char* getAttack()
{
return breed_.getAttack();
}
private:
int health_; // 当前血值
Breed& breed_;
};
上面这两个非常简单的类就是我们类型对象模式的核心思想了,下面再讲的都是一些周边扩展福利!
3,我们再来探讨下可能遇到的一些其它问题的决策:
问题一:目前我们实现的类型对象模式,它要创建一种类型会是这样:
Breed *someBreed = new Breed(...);
Monster * monster = new Monster(someBreed);
其实我们可以进一步封装,使创建新对象尽量接触少的类,将它们绑定在一起,让它更像类型,首先,把Breed类设置成Monster类的友元类,并把Monster构造设为私有属性,构造新种类全交个Breed类型对象。
class Monster
{
friend class Breed;
public:
const char* getAttack() { return breed_.getAttack(); }
private:
Monster(Breed& breed)
: health_(breed.getHealth()),
breed_(breed)
{}
int health_; // 当前血值
Breed& breed_;
};
class Breed
{
public:
Monster* newMonster() { return new Monster(*this); }
// Previous Breed code...
};
这样之后,我们再创建新类型就变成这样了:
Monster* monster = someBreed.newMonster();
问题二:如果我们有这样的需求,需要在不同类型的怪物间分享数据,那这要怎么做呢?分享数据,继承当然是名副其实,但是我们的类型对象模式没有继承,这时可以这样模拟:
class Breed
{
public:
Breed(Breed* parent, int health, const char* attack)
: parent_(parent),
health_(health),
attack_(attack)
{}
int getHealth();
const char* getAttack();
private:
Breed* parent_;
int health_; // 初始血值
const char* attack_;
};
Breed(Breed* parent, int health, const char* attack)
: health_(health),
attack_(attack)
{
// 继承没有重载的属性
if (parent != NULL)
{
if (health == 0) health_ = parent->getHealth();
if (attack == NULL) attack_ = parent->getAttack();
}
}
通过外部传入的方式来模拟继承属性,这样就可以在不同类型对象之间分享数据了。
好,类型对象就先讲这么多了,结束!