想象一下以下情况:
我想创建各种怪物工厂。这些怪物工厂根据struct
数组提供的数据创建怪物。怪物只在这些统计数据上有所不同,因此为每个怪物创建一个子类是过度的。
struct monster_data
{
int HP;
int strength;
int speed;
// even more attributes
};
班级monster
可以根据monster_data
来处理怪物的所有行为:
class monster
{
public:
monster(monster_data* initial_stats, int length);
void attack();
void walk();
void die();
// and so forth
};
到目前为止,这么好。现在我有一个类monster_factory
,它基于硬编码的monster_data
数组创建怪物:
const monster_data district1_monsters[]
{
{ 500, 20, 4 }, // monster1
{ 550, 5, 12 }, // monster2
{ 420, 8, 10 }, // monster3
{ 310, 30, 7 }, // monster4
// 100 more monsters
};
class monster_factory
{
public:
monster_factory(monster_data* monster_to_create) ;
monster* create_random_monster();
};
我的问题是,我必须为几个地区支持多个monster_factories
,但这些地区的名单略有不同:
const monster_data district1_monsters[]
{
{ 500, 20, 4 }, // monster1
{ 550, 5, 12 }, // monster2
{ 420, 8, 10 }, // monster3
{ 310, 30, 7 }, // monster4
// 100 more monsters
};
const monster_data district2_monsters[]
{
{ 500, 20, 4 }, // monster1
{ 750, 5, 12 }, // MONSTER2B <<
{ 420, 8, 10 }, // monster3
{ 310, 30, 7 }, // monster4
// monsters 5 - 80 from district 1
};
const monster_data district3_monsters[]
{
{ 500, 20, 4 }, // monster1
{ 550, 5, 12 }, // monster2
{ 720, 80, 10 }, // MONSTER3B <<
{ 310, 30, 7 }, // monster4
// monsters 8 - 90 from district 1
};
我希望以某种方式继承它,而不是复制和粘贴数组数据,因为不同版本之间的数据大致相同。复制整个struct
数组声明只是为了有一个稍微不同的变体似乎是错误的方式。太糟糕了,第2区和第3区只是不附加数据,它们会修改和省略现有条目。当然,他们也改变了不止一个怪物。
此外,第1区的怪物数据的变化也应适用于第2区和第3区。
另一个问题是,有些地区的怪物数据与地区1,2和3完全无关。
const monster_data district4_monsters[]
{
{ 100, 20, 10 }, // monster 401
{ 200, 50, 20 }, // monster 402
{ 300, 40, 5 }, // monster 403
{ 400, 30, 30 }, // monster 404
// 20 more monsters unrelated to district 1,2 & 3
};
现在回答问题:如何更改概述的设计,以避免冗余的monster_data
声明,并且可以添加从monster_data
派生出来的地区现有的声明还是使用全新的声明?
奖励积分,如果您的设计确保每个怪物统计列表的变体只能有一个工厂实例。
答案 0 :(得分:2)
decorator pattern可以通过使用每层中的更改来修饰“默认”表来优雅地解决这个问题:
class MonsterTable
{
public:
virtual monster_data const* getMonsterForIndex(int i)=0;
};
class DefaultMonsterTable : public MonsterTable
{
public:
monster_data const* getMonsterForIndex(int i)
{
return district1_monsters+i;
}
};
class OverlayMonsterTable : public MonsterTable
{
public:
//public for brevity, should be private in real code - can also be std::map
std::unordered_map<int, monster_data> OverlayData;
// Init this with the "previous layer", which is always the Default table in your examples
MonsterTable* Decorated;
monster_data const* getMonsterForIndex(int i)
{
typedef std::unordered_map<VGLindex, monster_data>::const_iterator Iterator;
Iterator Overlay=OverlayData.find(i);
if (Overlay!=OverlayData.end()) // Monster data was changed in this layer
return &Overlay->second;
return Decorated->getMonsterFromIndex(i); // Defer to next layer
}
};
然后,您可以将更高区域中的所有“更改”添加到OverlayData,并让OverlayMonsterTable引用默认表(zone1)。
为了支持省略数据,你可以添加另一个重新映射索引的装饰“层”(例如,map [0 ... 80]到[0 ... 10],[30 ... 100]) ,或将此功能集成到现有的OverlayMonsterTable中。无论哪种方式,您都有充分的灵活性。例如:
class OmitMonsterTable : public MonsterTable
{
public:
int OmitBegin, OmitEnd;
MonsterTable* Decorated;
monster_data const* getMonsterForIndex(int i)
{
if (i > OmitBegin)
i += OmitEnd;
return Decorated->getMonsterForIndex(i);
}
};
你的工厂只需要一个MonsterTable指针/参考。
答案 1 :(得分:1)
你继续使用“inherit”这个词,但我绝对不会考虑继承,你只有一种行为,即一种工厂类,你只想用不同的数据初始化工厂。
我会有一个包含所有不同monster_data
值的大数组:
const monster_data all_data[] = {
// district1_monsters
{ 500, 20, 4 }, // monster1
{ 550, 5, 12 }, // monster2
{ 420, 8, 10 }, // monster3
{ 310, 30, 7 }, // monster4
// 100 more monsters
// ...
// district 2 monsters (index 104)
{ 750, 5, 12 }, // MONSTER2B <<
// district 3 monsters (index 105)
{ 720, 80, 10 }, // MONSTER3B <<
// district4 monsters (index 106)
{ 100, 20, 10 },
{ 200, 50, 20 },
{ 300, 40, 5 },
{ 400, 30, 30 },
// 20 more monsters unrelated to district 1,2 & 3
// ...
};
然后创建包含正确序列的序列:
typedef std::vector<monster_data> data_seq;
data_seq district1_data(all_data, all_data + 104);
data_seq district2_data(all_data, all_data + 80);
district2_data[2] = all_data[104];
data_seq district3_data(all_data, all_data + 3);
district3_data.push_back( all_data[105] );
district3_data.insert(district3_data.end(), all_data+8, all_data+90);
data_seq district4_data(all_data+106, all_data + 126);
然后从这些序列中创建工厂:
class monster_factory
{
public:
monster_factory(const data_seq& monsters) ;
monster* create_random_monster();
};
monster_factory district1_factory(district1_data);
monster_factory district2_factory(district2_data);
monster_factory district3_factory(district3_data);
monster_factory district4_factory(district4_data);
如果monster_data
类型只有三个整数应该没问题。如果它是一个更大的类,那么你可以使data_seq
成为vector<const monster_data*>
,因此它只保存指向all_data
数组元素的指针。这避免了复制monster_data
对象,它们只存在于主all_data
数组中,其他所有内容都通过指针引用那些主副本。这将需要更多的工作来创建矢量对象,因为你需要用数组元素的地址填充它,而不是元素的简单副本,但这是你在程序启动时只需要做的事情,所以写一些代码来做正确的事是值得的:
struct address_of {
const monster_data* operator()(const monster_data& m) const
{ return &m; }
};
// ...
typedef std::vector<const monster_data*> data_seq;
data_seq district1_data;
std::transform(all_data, all_data + 104,
std::back_inserter(district1_data), address_of());
data_seq district2_data;
std::transform(all_data, all_data + 80,
std::back_inserter(district2_data), address_of());
district2_data[2] = &all_data[104];
data_seq district3_data;
std::transform(all_data, all_data + 3,
std::back_inserter(district3_data), address_of());
district3_data.push_back( all_data[105] );
std::transform(all_data+8, all_data + 90,
std::back_inserter(district3_data), address_of());
data_seq district4_data;
std::transform(all_data+106, all_data + 126,
std::back_inserter(district4_data), address_of());
初始化每个区的序列的另一种可能更可维护的方法是为每个区域建立索引数组:
int district1_indices[] = { 0, 1, 2, 3, 4, ... 103 };
int district2_indices[] = { 0, 1, 104, 3, 4, ... 79 };
int district3_indices[] = { 0, 1, 2, 105, 7, 8, 9, 10 ... 89 };
int district4_indices[] = { 106, 107, 108, 109 ... 125 };
然后用其中一个数组(及其长度)构造一个工厂。工厂可以从列表中选择一个索引,然后使用它索引到all_data
以获得monster_data
。
答案 2 :(得分:1)
将数据存储在二进制文件中通常是不好的做法,并且无法扩展,特别是如果它将是大量数据。定义支持简单数据继承的迷你语言然后将其解析为包含unordered_map
的类应该不会有太多麻烦。如果您需要,这将使您能够实现简单的数据共享和更复杂的属性系统。
答案 3 :(得分:0)
当我要求一个怪物时,我有一个工厂通过该区。 然后我可以做类似的事情(仅限伪代码)
getMonster(int district)
{
monster_data dat = getRandomBaseMonster();
// dat has to be a copy so we don't stomp in the base data
if (district == 2) {
dat.HP += 10;
}
return dat;
}
答案 4 :(得分:0)
一种解决方案可能是拥有一个包含“标准”怪物数据的基表,然后为每个区域提供一个只包含修改过的怪物列表的表。
类似的东西:
const monster_data base_monsters[] = {
{ 500, 20, 4 }, // monster1
{ 550, 5, 12 }, // monster2
{ 420, 8, 10 }, // monster3
{ 310, 30, 7 }, // monster4
// 100 more monsters
};
struct monster_change_data
{
int monster; /* Index into base table */
struct monster_data data; /* Modified monster data */
};
const struct monster_change_data district2_monsters[] = {
{ 1, { 750, 5, 12 } }, // MONSTER2B
};
const struct monster_change_data district3_monsters[] = {
{ 2, { 720, 80, 10 } }, // MONSTER3B
};
这样你只需要列出改变过的怪物。
答案 5 :(得分:0)
为了完整起见,我会在ltjax发布他的答案之前发布我想出的设计,尽管我的是劣等。因为它采用了不同的方法,但其他人可能会感兴趣。
它将工厂与其表格结合在一起,因为表格本身没什么意义。 表的填充是在工厂的构造函数中完成的。这样,其他工厂可以继承构造函数并对表进行更改。 缺点是每个工厂都创建自己的全表,因此在运行时存储冗余数据。至少维护变得更容易。
通过将帮助器方法add
,replace
和remove
移动到单独的表类来正确封装它们,可以改进这一点。但在这种情况下,monster_factory_abstract
基本上是空的,IMO。
class monster_factory_abstract
{
private:
monster_data* table; // or map with sequential indices
int table_length;
protected:
// add monster to table
void add(int HP, int strength, int speed, etc.);
// index starts with one to match monster names in this example
void replace(int index, int HP, int strength, int speed, etc.);
void remove(int index); // nulls an entry
void remove(int from, int to);
public:
virtual monster* create_random_monster();
}
class monster_factory_district1 : public monster_factory_abstract
{
public:
monster_factory_district1()
{
table_length = 0;
add( 500, 20, 4 ); // monster1
add( 550, 5, 12 ); // monster2
add( 420, 8, 10 ); // monster3
add( 310, 30, 7 ); // monster4
// add 100 more monsters
}
};
class monster_factory_district2 : public monster_factory_district1
{
public:
monster_factory_district2() : monster_factory_district1
{
replace( 2, 750, 5, 12 ); // MONSTER2B <<
remove(81, 100);
}
};
class monster_factory_district3 : public monster_factory_district1
{
public:
monster_factory_district3() : monster_factory_district1
{
replace( 3, 720, 80, 10 ); // MONSTER3B <<
remove(5, 8);
remove(91, 100);
}
};
class monster_factory_district4 : public monster_factory_abstract
{
public:
monster_factory_district4() : monster_factory_abstract
{
table_length = 0;
add( 100, 20, 10 ); // monster 401
add( 200, 50, 20 ); // monster 402
add( 300, 40, 5 ); // monster 403
add( 400, 30, 30 ); // monster 404
}
};