0

trying to wrap my head around javascript composition using assign. The property on my base object is unexpectedly being shared between instances. What am I doing wrong? I have...

Stat.js:

import assign from 'object-assign';
var Stat = assign({}, {
    _value: undefined,

    get: function() {
        return this._value;
    },

    set: function(n) { 
        var max = this.getMax();

        if(n > max) {
            this._value = max;
        } else {
            this._value = n; 
        }
    },

    getMax: function() {
        return 1;
    }
});
module.exports = Stat;

HitPoints.js:

import assign from 'object-assign'
import Stat from './Stat.js';

var HitPoints = assign({}, Stat, {        
    getMax: function() {
        return 100;
    }
});
module.exports = HitPoints;

Unit.js:

import assign from 'object-assign';
import HitPoints from 'HitPoints.js';

var Unit = assign({}, {
        hp: Object.create(HitPoints)
    }
);
module.exports = Unit; 

Usage:

import Unit from 'Unit.js';

var u1 = Object.create(Unit);
console.log( u1.hp.get() ); // undefined - ok
u1.hp.set(11);
console.log( u1.hp.get() ); // 11 - ok

var u2 = Object.create(Unit);
console.log( u2.hp.get() ); // 11 - ???
u2.hp.set(22);
console.log( u1.hp.get() ); // 22 - ???
console.log( u2.hp.get() ); // 22

Thanks for your help...

Robert Lombardo
  • 152
  • 1
  • 9
  • There is no benefit to use `assign` here. `hp` is shared because both `u1` and `u2` have `Unit` (a single object) as prototype. `Object.create(Unit)` doesn't create a *copy* of `Unit`, if that's what you thought. – Felix Kling Mar 21 '16 at 03:37
  • @Felix Kling I see... so, how should I be doing this. I'm a bit of a javascript n00b, coming from AS3. The above was inspired by [this explanation](http://www.datchley.name/delegate-object-to-classical-inheritance/) of javascript objects. – Robert Lombardo Mar 21 '16 at 04:13
  • I'd recommend to use a constructor function: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript#Custom_objects . – Felix Kling Mar 21 '16 at 04:34
  • If you do need to use assign, any particular reason you are not using `Object.assign`? –  Mar 21 '16 at 04:41
  • @torazaburo says [here](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) it's not particularly well supported yet... – Robert Lombardo Mar 21 '16 at 04:47
  • To elaborate a little on "coming from AS3," I mean that I am porting a lot of AS3 code that uses classical inheritance. Actually the "Unit" I am working with is about 5 steps down an inheritance chain, and I'm trying to refactor into a composition pattern... maybe I should just go with es6 classes? I just keep reading that this is unwise, and that generally speaking composition is better. Trying to level up here, haha. – Robert Lombardo Mar 21 '16 at 04:52
  • @RobertLombardo if you port AS3 directly to the `class` structure, it will likely mostly work, without incident; if you want composition rather than hierarchy, it requires a very different structure, and a different way of looking at the world. – Norguard Mar 21 '16 at 04:58
  • where's the 'like' button on this thing? different-way-of-looking-at-the-world is my middle name! said i was trying to level up... ;) – Robert Lombardo Mar 21 '16 at 05:04

2 Answers2

6

For starters, a quick example of why people don't want you using class.
I don't necessarily hate class, but 90% of the reason to use class is to get inheritance, and while it's occasionally helpful, it can frequently be very painful.

class Person { }


class ArmedPerson extends Person {
  constructor (details) {
    super(details);
    const gun = new Gun();
    this.equipment = { gun };
  }

  attack (target) {
    this.equipment.gun.fireAt(target);
  }
}


class Civilian extends Person { }


class ArmedCivilian extends ArmedPerson {
  /* ... shouldn't it be extending Civilian?
    Maybe all Civilians should be armed?
    Is this why most games take place in the US?
  */
}


class Soldier extends ArmedPerson {
  constructor (personDetails) {
    super(personDetails);
  }
}


class UnarmedSoldier extends Soldier {
  /* HOW DO I TAKE HIS GUN AWAY? */
  constructor (personDetails) {
    super(personDetails);
  }
  attack () {
    /* I know he has a gun, and anybody hacking the game can use it, but what do I do here? */
  }
}

class inheritance has shown itself to be one of those things that people have misused terribly, for the past 30+ years (just like every other useful tool out there).

Rather than inheritance, we can look at composition (via Dependency Inversion).

class Soldier {
  constructor (personDetails, gun) {
    /*...setup...*/
    this.equipment = { gun };
    this.selectedWeapon = gun;
  }

  attack (target) {
    this.selectedWeapon.fireAt(target);
  }
}


const soldier = new Soldier({ /*...details... */ }, new Gun());

Not a lot has changed, in terms of the end-result we wanted... we've been able to simplify really a lot, and now we can even give him a method to swap guns if we want, all because rather than bolting the gun into something that he inherits from, we're handing him a gun when we first meet him. It could be any type of gun we want, as long as it can still be fired in a similar fashion.

The question arises: are there better ways of making things reusable, then, if inheritance is completely off the table?

To that I say: inheritance shouldn't be completely off the table... ...it should just be so far off to the side that it should be an "aha" moment, when you discover that it really is the best and cleanest way to accomplish something (rather than attempting to inherit from .......something, anything, right now!).

Various languages have a concept referred to as Traits or Mix-Ins.
In something like Java, a close-ish approximation is Interfaces.
I'm not a huge fan of Interfaces (the structure, not the concept - love the concept).
In Java, Interfaces make you do more work, because they have you define the function, what the function takes, what it returns... ...but you can't give it any default behaviour (traditionally), so if you have 14 objects which implement the same interface, that's the same method you write out 14 times (plus the signature for the interface). Sometimes, those methods are going to be completely different in the specifics of the implementation; that's fine... ...sometimes, they'll be the exact same as what you intended when you wrote the interface to begin with.

That's less okay. Queue Traits; these are things which you define the interface of, define the behaviour for, and then copy onto your object. In JS, we can even have some closure safety around them, by injecting context that they get to work from, rather than letting them assume they get to mess around with the entirety of this.

const Movable = (pos) => ({
  up (distance) { pos.y += distance; },
  down (distance) { pos.y -= distance; },
  left (distance) { pos.x -= distance; },
  right (distance) { pos.x += distance; }
});


class Point {
  constructor (x, y) {
    Object.assign(this, { x, y });
  }
}

class Person {
  constructor (position) {
    Object.assign(this, { position }, Movable(position));
  }
}


const person = new Person( new Point(0, 0) );

person.up( 20 );
person.position.y; // 20

If you'll note, Movable is returning a new instance of an object, with methods which change values on position. That object is having its methods copied onto the instance of person.

I can now create as many of these Traits as I'd like, and copy them onto as many objects as I'd like, and get reuse that way.

Norguard
  • 26,167
  • 5
  • 41
  • 49
-1

Well, this worked...

Stat.js:

var Stat = {
    get: function() {
        return this._value;
    },

    set: function(n) { 
        var max = this.getMax();

        if(n > max) {
            this._value = max;
        } else {
            this._value = n; 
        }
    },

    getMax: function() {
        return 1;
    }
}

HitPoints.js:

var HitPoints = function() {
    return assign(Object.create(Stat), {    
        getMax: function() {
            return 100;
        }
    });
}

Unit.js:

var Unit = function() {
    return assign({},
        Object.create(XYPiece),
        Object.create(TeamMember),
        {
             hp: HitPoints()     
        }
    );                       
}

Usage:

var u1 = Unit();
console.log( u1.hp.get() ); // undefined
u1.hp.set(11);
console.log( u1.hp.get() ); // 11

var u2 = Unit();
console.log( u2.hp.get() ); // undefined
u2.hp.set(22);
console.log( u1.hp.get() ); // 11
console.log( u2.hp.get() ); // 22

This article helped. Hooray!!!

Still, tho, if this is fundamentally an idiotic way to go about it, tell me...

Robert Lombardo
  • 152
  • 1
  • 9