Imagine the following situation:
I want to create various monster factories. These monster factories create monsters based on the data provided by a struct
array. The monsters only differ in these stats, therefore creating a subclass for each monster is overkill.
struct monster_data
{
int HP;
int strength;
int speed;
// even more attributes
};
A class monster
can handle all the behavior of a monster based on a monster_data
:
class monster
{
public:
monster(monster_data* initial_stats, int length);
void attack();
void walk();
void die();
// and so forth
};
So far, so good. Now I have a class monster_factory
that creates monsters based on a hard coded monster_data
array:
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();
};
My problem is that I have to support several monster_factories
for several districts with with minor differences in the lists:
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
};
Instead of copying and pasting the array data, I would like to somehow inherit from it, because the data stays mostly the same between the various versions. Copying the whole struct
array declaration just to have a slightly different variant seems like the wrong way. Too bad that district 2 and 3 just don't append data, they modify and omit existing entries. Of course they change more than one monster, too.
In addition changes on the monster data of district 1 should apply to district 2 and 3 as well.
Another problem is that there are districts that will have monster data completely unrelated to districts 1,2 and 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
};
Now to the question: How can the outlined design be changed, so that redundant monster_data
declarations are avoided and that districts can be added that either derive their monster_data
from an existing declaration or use a completely new one?
Bonus points, if your design ensures that there can only be one factory instance for every variant of the monster stats list.
This can be solved elegantly by the decorator pattern by decorating the "default" table with the changes in each layer:
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
}
};
You would then add all "changes" in higher districts to the OverlayData and let the OverlayMonsterTable refer to the default table (district1).
To support omitting of data, you can either add another decorator "layer" that remaps indices (for example, maps [0...80] to [0...10], [30...100]), or integrate this functionality into the existing OverlayMonsterTable. Either way, you have full flexibility. For example:
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);
}
};
Your factory would just take a MonsterTable pointer/reference.