Search code examples
javascriptinheritanceprototypal-inheritance

Update properties of all objects of the same type after instantiation


I'm currently facing a tricky problem which I am not able to solve in javascript. I have a function object Unit for example. This is the base object.

There are two function objects Tank and AirDefense which are prototypal inheriting from Unit

A "child" of Unit has two properties level (normal integer) and damagePerSecond (an array of integers where the index represents the level and the value the damage per second on that level).

level should be a "static" property, so whenever I change the level of an instance of AirDefense or Tank every instance of it that was previously created must be updated and apply the changed level

However there also should be an interface to change the level statically, so that I don't have to call the setLevel method on a concrete instance but rather on the function object itself.

The problem is that I am implementing a method called getDamagePerSecond() which returns the damage per second for the current level. But the current level of the concrete child object is not known by the parent.

I cannot use a static property on the base object because it would set the level for both objects Tank and AirDefense but the levels should be seperated for each of those child objects.

tl;dr: I want to use a child objects static property from the parent object, but I don't know the child in the parent object yet.

I'll provide what I have tried so far to clear up how these objects work together.

Unit

function Unit(){
    this.level;
    this.damagePerSecond;   
}
Unit.prototype.getDamagePerSecond = function(){
    return this.damagePerSecond[this.level];
};

Unit.prototype.getLevel = function(){
    return this.level+1;
}

Tank

function Tank(){

    this.level = Tank.level || 0;
    this.dps = [5,10,15];

}

Tank.prototype = new Unit();
Tank.setLevel = function(level){
    Tank.level = level-1;   
}

AirDefense

function AirDefense(){

    this.level = AirDefense.level || 0;
    this.dps = [1,5,9];

}

AirDefense.prototype = new Unit();
AirDefense.setLevel = function(level){
    AirDefense.level = level-1; 
}

Main (Correct Result)

Tank.setLevel(5);

var t = new Tank();
var a = new AirDefense();

console.log(t.getLevel()); //outputs 5
console.log(a.getLevel()); //outputs 1

Main (Wrong Result)

var t = new Tank();
Tank.setLevel(5);
var a = new AirDefense();

console.log(t.getLevel()); //outputs 1 - should be 5
console.log(a.getLevel()); //outputs 1

What I basically did was to fake static properties in the concrete objects. Level is a static property on my child object. During instantiation I have an additional instance property with the level which will be set to the value of the fake static property.

But there are several problems arising here:

  1. I can only set a level for an object type before it is instantiated. But I want to change the level of both, all objects that are already instantiated and those that will be in the future.

  2. Using this approach I need to implement a setLevel() method on all of my child objects. I think this is the opposite of dry.

  3. By providing a static interface it is impossible to directly message concrete instances that do already exist

If I used terms that are not usual when speaking about prototyping I'm sorry for that. I come from languages that have a classic OO-approach.

I'm not that familiar with prototypal inheritance just yet, so I hope somebody can give me a wink into the right direction.


Solution

  • So level should be shared for all Tank instances? You can do something like this:

    function Unit(){}
    Unit.prototype.getLevel = function(){
        return this.level+1;
    };
    function Tank(){
        //level should be shared by all Tanks
        //  setting this.level shadows Tank.prototype.level
        //  so let's not do that
    //    this.level = Tank.level || 0;
    }
    //not sure why you want a Unit instance to be
    //  prototype of all Tanks
    Tank.prototype = Object.create(Unit.prototype);
    Tank.prototype.constructor=Tank;
    Tank.prototype.level=5;
    Tank.prototype.setLevel = function(level){
      //set Tank.prototype.level so it's shared
      //  for all tank instances
      Tank.prototype.level = level-1;   
    }
    //same for AirDefense
    var t1 = new Tank();
    var t2 = new Tank();
    console.log("t1 level:",t1.level,"t2 level",t2.level);
    console.log("t1 getLevel:",t1.getLevel(),"t2 getLevel",t2.getLevel());
    t1.setLevel(11);
    console.log("t1 level:",t1.level,"t2 level",t2.level);
    console.log("t1 getLevel:",t1.getLevel(),"t2 getLevel",t2.getLevel());
    

    Or have an object member on Tank.prototype and mutate that.

    function Unit(){}
    Unit.prototype.getLevel = function(){
        return this.shared.level+1;
    };
    Unit.prototype.setLevel = function(level){
        this.shared.level=level;
    };
    function Tank(){}
    Tank.prototype = Object.create(Unit.prototype);
    Tank.prototype.constructor=Tank;
    Tank.prototype.shared={};
    Tank.prototype.shared.level=5;
    var t1 = new Tank();
    var t2 = new Tank();
    console.log("t1 level:",t1.shared.level,"t2 level",t2.shared.level);
    console.log("t1 getLevel:",t1.getLevel(),"t2 getLevel",t2.getLevel());
    t1.setLevel(11);
    console.log("t1 level:",t1.shared.level,"t2 level",t2.shared.level);
    console.log("t1 getLevel:",t1.getLevel(),"t2 getLevel",t2.getLevel());
    

    More on constructor functions and prototype here: https://stackoverflow.com/a/16063711/1641941

    UPDATE

    Demonstrate inheriting instance specific and shared (prototype) members. A function is usually on the prototype because it does the same for all instances. A constructor can also initialize an instance specific member (such as damagePerSecond). No need to initialize it in both Tank and AirDefense as Unit can take care of it:

    function Unit(args){
      this.damagePerSecond=args.damagePerSecond;
      //..bunch of other instance specific members
    }
    
    function Tank(){
      //Tank is a Unit so we can take all instance specific Unit members
      //inherit instance specific members from Unit
      Unit.call(this, {damagePerSecond:[2,3,4]});
    }
    //inherit shared members from Unit
    //... Tank.prototype = Object.create(Unit.pro ... bla bla bla
    var t = new Tank();
    console.log(t.damagePerSecond);//=[2,3,4]
    

    The args is an object that can be passed to the constructor I could pass it to Tank and Tank can pass it to Unit the following way:

    function Tank(args){
      args=args||{};//don't worry if no args are passed
      args.damagePerSecond=args.damagePerSecond||[2,3,4];//passed arg or default
      //... and a bunch of other stuff
      Unit.call(this,args);
    }
    
    var t = new Tank();//all defaults
    var t1 = new Tank({damagePerSecond:[7,8,9]});//super Tank
    

    Using such a pattern is very helpful when you have functions calling other functions. When you define them strict like someFunction(who, what, when) and you'll have a long chain of functions you'll have a lot of typing to do when an extra variable named where needs to be added in the beginning but is only used at the end.

    Most of this is covered by the link I posted before.