Prototype Pattern causes code repetition between "actual object" and "prototype"

129 views Asked by At

After adopting Prototype Pattern into a game, it pleasantly improved maintainability of my code.

However, I have started to fear that when the actual object become more detail-customized,
I tend to code the corresponding prototype to become more like a copy of the actual object.

(notice #v.2 in code)

class Prototype{ //aka. "fake object"
    public: Vec3 position;
    //bool isShootable;            //(#v.2)   //#X
    //float delayBetweenShoot;     //(#v.2)
    //float hp;                    //(#v.2)
};

class Actual{ //aka. "actual object"
    public: PhysicObject* rigidBody=nullptr;
    //bool isShootable;            //(#v.2)   //#X
    //float delayBetweenShoot;     //(#v.2)
    //float hp;                    //(#v.2)
};

int main(){ // simplify 
    Prototype prototype; 
    prototype.position = Vec3(1,2,3);
    //^ end prototype creation
    //v these lines are inside a function, but it simply works like this
    PhysicObject* phy=new PhysicObject(prototype.position);
    Actual actual;
    actual.rigidBody=phy;
    //actual.isShootable      =prototype.isShootable;         //(#v.2)  #Y
    //actual.delayBetweenShoot=prototype.delayBetweenShoot;   //(#v.2)
    //actual.hp               =prototype.hp;                  //(#v.2) 
    gameLogic.add(actual); //roughly speaking
}

There are two bad signals (#v.2):-
1. repetitive code in Prototype vs Actual (#X)
2. tedious copying of fields. (#Y)

Thus, I think something start to go wrong.
This pattern may naturally cause new maintainability issues.

In the real situation, the actual2 contains another actual1.
To adopt Prototype Pattern, I use a corresponding prototype2 inside another corresponding prototype1 :-

class Actual1{
    //... some fields e.g. A1 a1; A2 a2; A3 a3;
};
class Actual2{
    //... some other field e.g. B1 B2 B3
    Actual1 actual1;
};
class Prototype1{
    //... some fields e.g. very similar to A1 A2 A3
};
class Prototype2{
    //... some other field e.g. very similar to B1 B2 B3
    Prototype1 prototype1;
};

Question

  • (1) Is it common that Prototype Pattern create new maintainability issues?
  • (2) If yes, how to avoid it?
  • (3) If no, where am I wrong (especially the style of coding)? ... or this trend (of repetitive code) is not really an issue at all (i.e. I just panic.)?

My poor solution

I think it might be a good idea to encapsulate the repetitive part into a single structure named Settings.

class Settings{
    bool isShootable;           
    float delayBetweenShoot;    
    float hp;                   
};

class Prototype{ //aka. "fake object"
    public: Vec3 position;
    Settings settings;   //(#v.2)
};

class Actual{ //aka. "real object"
    public: PhysicObject* rigidBody=nullptr;
    Settings settings;   //(#v.2)
};

However, it might increase unfavorable cohesion (i.e. glue or strong relation) between Prototype and Actual. Thus, it may leader to another new maintainability problem again.

2

There are 2 answers

2
Joel Cornett On

You can avoid the unnecessary duplication by subclassing your 'actuals' from the prototype. For example:

struct Prototype {
    bool isShootable = false;
    float delayBetweenShoot = DEFAULT_DELAY;
    float hp = DEFAULT_HP;
    Vec3 position = STARTING_POSITION;
    ...
};

struct Actual : Prototype {
    PhysicObject* rigidBody;
    Actual() : Prototype(), rigidBody(new PhysicObject(position)) {}
}

int main() {
    Actual actual;
    // now you can access these methods
    if (actual.isShootable) {
        ...
    }
    ...
}

Your intuition is correct in that by grouping 'common' fields together, you increase the coupling between those fields. In some sense there is a tradeoff between coupling and code duplication. It is up to you to determine what is an acceptable compromise that best suits your application.

2
BitTickler On

Probably using different types (with the ensuing double book keeping) is not the best interpretation of that design pattern.

See below for an approach which avoids the double book keeping and still holds the benefits of basic idea - to preconfigure a sample or template object once, then use that instance to initialize many other instances.

class A {
    int32_t id;
    bool shootable;
    bool moveable;
    bool destructable;
public:
    // A template instance specific constructor.
    A(bool shoot, bool move, bool destruct)
        : id(-1) 
        , shootable(shoot)
        , moveable(move)
        , destructable(destruct)
   {
   }
   // One or more "real" instance constructors.
   A(int32_t idval, const A& source)
        : id(idval)
        , shootable(source.shootable)
        , moveable(source.moveable)
        , destructable(source.destructable)
   {
   }
   // ...
};

int main(int argc, const char *argv[])
{
    A kind(true,false,true);
    A instance0(1,kind);
    A instance1(2,kind);
    return 0;
}

As a variation of the above idea, you could as well store a reference to the template instance and indeed use 2 types.

class UnitType
{
    int32_t hp;
    bool ranged;
    //...
};
class Unit
{
    int32_t id;
    const UnitType *type;
    // more data

public:
    Unit(int32_t idval, const UnitType* unitType)
    : id(idval)
    , type(unitType)
    {
    }
    //...
};

Of course, then the UnitType instance should not be writable by instances. And once, there is also e.g. currentHp, you have another form of duplication to handle. Plus, you need to ensure the life time for the UnitType template instance exceeds that of each Unit instance using it.