Good Practice: How can I ensure a JavaScript constructor has access to mixin functions?
Asked Answered
O

3

6

As part of an RPG game back-end, I want to be able to apply temporary effects to the characters. The nature of these effects could vary quite a lot, but I want to keep the method of defining them very straightforward.

I'm using custom event handling as a mixin:

var EvtObject = {};
$rpg.Event.enable(EvtObject); // Add the 3 methods and set EvtObject._events = {}

I want to define Auras (the temporary effects) as a constructor with event handling code:

var MyAura = function(any, args){
  this.group = "classification";
  this.on( "tick", function(){} );
  this.on( "remove", function(){} );
};

Then applied as MyCharacter.addAura(new MyAura(any, args));. As you can see, I want the this.on() function to be available in the constructor. If I extend the MyAura prototype with the mixin ($rpg.Event.enable(MyAura.prototype)) then every instance of MyAura references the same _events object from the prototype.

I want to know if the following solution is good practice:

Aura.create = function(Constructor)
{
    Constructor.prototype = Aura.getPrototype(); // Aura specific prototype

    return function()
    {
        var newAura = Object.create(Constructor.prototype);
        $rpg.Event.enable( newAura );
        Constructor.apply( newAura, arguments );
        return newAura;
    };

};

// Then creating new Auras:
var MyAura = $rpg.Aura.create(function(any, args){
  // this.on() is available
};
Ominous answered 9/10, 2015 at 23:19 Comment(5)
You may as well change the mixin so that it's lazily instanciating the _events member on the object (when on is called for instance). That's the approach taken by asEvented.Veal
What about a compositionnal approach to mixins? Aura = eventMixin.mixInto(Aura); where this applies the mixin and returns a new composed constructor. The only thing with that is the mixin application cannot be deferred since no one should be referencing the old constructor at this point.Veal
Thanks for your comments. I thought about instantiating _events as you described, I'm not quite sure what put me off of that idea, so that may be what I use eventually. Could you elaborate what you mean by "compositional"?Ominous
Something like this jsfiddle.net/28bL8rx0/1 ... there's a lot a boilerplate behind newMixin but mixin usage becomes very simple and the same goes for declaring new mixin types.Veal
Great example, thanks! This looks like a really neat way to implement.Ominous
S
1

There are characters (instances) acting in this game. Occasionally there is the need of enriching them by additional behavior or applying new roles, like an aura, to some of them.

A clean approach already uses traits/mixins at this point ...

If I extend the MyAura prototype with the mixin $rpg.Event.enable(MyAura.prototype)

... as it happens with the OP's provided example.

Since JavaScript at it's core level does provide mixin mechanics for free by a Function and delegation based pattern, ...

I want to define Auras (the temporary effects) as a constructor with event handling code:

... an Aura's implementation should be switched from it's constructor approach towards the so called Flight Mixin approach ...

var withAura = (function (Event) { // mixin module implementation .

  var
    tickHandler   = function () {},
    removeHandler = function () {},

    Mixin = function withAura (config) { // not a constructor but a function based "flight mixin".
      var aura = this;

      Event.enable(aura);

      aura.group = config.group;

      aura.on("tick", tickHandler);     // referencing ...
      aura.on("remove", removeHandler); // ...  shared code.
    }
  ;
  return Mixin;

}($rpg.Event));


var Player = (function () { // factory module implementation.

  var
    // ...
    // ...
    createPlayer = function (config) { // factory.
      var player = {};
      // ...
      // ...
      return player;
    }
  ;

  return {
    create: createPlayer
  };

}());


var somePlayer = Player.create(/*config*/);

At any given point within the game, enriching a players skill or behavioral set with an Aura, will be as straightforward as a one line delegation call ...

withAura.call(somePlayer, {group: "classification"});

Removing such behavior is another task and can be discussed afterwards/separately.

Smut answered 12/10, 2015 at 10:59 Comment(2)
I really like where this is going, so thanks for your time! Certainly feel I'm working towards some better code. However I'm not certain how I would implement this pattern to create varying auras. The thing jumping out to me is that, unless I've misread, the events are being enabled upon and applied to the Player class rather than the Aura object itself.Ominous
@Ominous - The answer will be too long. It requires an own answer block.Smut
S
0

I'm not certain how I would implement this pattern to create varying auras.

One hase to make a decision, either designing an Aura constructor-/factory-based and not cutting off prototypal delegation in order of being able going later for more aura variations making use of inheritance, or decomposing a fully-fledged Aura into various mixins, each of it with a well defined set of fine grained behavior.

The aura instance based approach most propably gets complicated where it relies on forwarding if one is going to "add" an aura instance (that in the given example is capable of event dispatching) onto a character ... MyCharacter.addAura(new MyAura(any, args)) ... The addAura method of a player's constructor needs to be implemented in a way that already takes into account that aura by itself features event-target methods. Thus this code has to come up with an own solution for forwarding in order to enable a player of event dispatching for the time carrying an aura along.

The next given boiled down but working example(s) might point to what just was said ...

var Player = (function () { // sort of factory module implementation.

  var
  // ...
  // ...

    addAura = function (player, aura) { // simplified forwarding or delegation.
      // ...
      player.on = function (type, handler) {

        aura.on(type, handler);               // forwarding.

      // please try switching it to delegation ...

      //aura.on.apply(player, arguments);     // delegation.
      //aura.on.call(player, type, handler);  // delegation.

        console.log("player.on :: player.getName() ", player.getName());
      };
      // ...
    },


    Constructor = function Player (name) {
      // ...
      // ...
        this.getName = function () {
          return name;
        };
      //this.addAura = function (aura) {
      //  addAura(this, aura)
      //};
    },


    createPlayer = function (config) { // factory.
      var
      // ...
      //player = (new Constructor)
        player = (new Constructor(config.name))
      // ...
      ;
    //player.name = config.name;

      player.addAura = function (aura) {
        addAura(player, aura)
      };

      return player;
    }
  ;

  return {
    create: createPlayer
  };

}());

...

var         // simplified aura object - for demonstration only.
  aura = {
    on: function (type, handler) {
      if (typeof handler == "function") {
        handler.call(this);
      }
      console.log("aura :: on - [this, arguments] ", this, arguments);
    }
  },
  p1 = Player.create({name:"p1"}),  // 1st character/player.
  p2 = Player.create({name:"p2"})   // 2nd character/player.
;

...

p1.addAura(aura);
p2.addAura(aura);

p1.on("tick", function () {console.log("tick :: this", this);});
p2.on("remove", function () {console.log("remove :: this", this);});

For all of the above, I personally always do prefer the function based mixin composition approach.

The thing jumping out to me is that, unless I've misread, the events are being enabled upon and applied to the Player class rather than the Aura object itself.

If one follows the approach of only dealing with player/character instances whereas anything else that can be seen as set of additional behaviors which a player can acquire or can get rid of (or that can be awarded to or can be withdrawn from any player) at runtime, then for the latter, one talks about role based composition. Thus both Observable and Aura are mixins.

There will be no such thing like an aura object.

If a player does not already feature observable behavior, but an aura behavior does rely on it, then the Aura mixin should make use of the former. A player's factory then does not deal with Observable but a player object will feature Observable behavior as soon as an Aura mixin gets applied to it.

... and now coming back again to ...

I'm not certain how I would implement this pattern to create varying auras.

Sticking to function based mixins there are again mainly two solutions. Firstly, as in the example I provided in my first answer, one can go for a sole but configurable Mixin. Or secondly, one does favor the already mentioned many aura mixin fragments.

var $rpg = {  // mocking and faking heavily.

  Event: {
    enable: function (observable) {

      observable.on = function (type, handler) {
        if (typeof handler == "function") {
          handler.call(this);
        }
        console.log("aura :: on - [this, arguments] ", this, arguments);
      };

      return observable;
    }
  }
};

...

var withAuraBase = function AuraBaseMixin (group) { // not a constructor but a simple function based "flight mixin".
  var auraEnabled = this;

  // ...
  $rpg.Event.enable(auraEnabled);
  // ...

  auraEnabled.group = group;
  // ...
};
var withFencinessFactorAura = function FencinessFactorAuraMixin (fencinessFactor) {
  // ...
  this.fencinessFactor = fencinessFactor;
  // ...
};
var withCheatingFeaturesAura = function CheatingFeaturesAuraMixin (config) {
  // ...
  this.cheatingFeatures = config;
  // ...
};

The Player factory code from above then will shrink to ...

var Player = (function () { // sort of factory module implementation.

  var
  // ...
  // ...
    Constructor = function Player (name) {
      // ...
      // ...
      this.getName = function () {
        return name;
      };
    },

    createPlayer = function (config) { // factory.
      var
      // ...
        player = (new Constructor(config.name))
      // ...
      ;
    //player.name = config.name;

      return player;
    }
  ;

  return {
    create: createPlayer
  };

}());

...

var
  p1 = Player.create({name:"player1"}), // 1st character/player.
  p2 = Player.create({name:"player2"})  // 2nd character/player.
;

// applying mixin examples differently.

withAuraBase.call(p1, "classification");
withAuraBase.call(p2, "classification");

withFencinessFactorAura.call(p1, 10);
withCheatingFeaturesAura.call(p2, {/*cheatingFeaturesConfig*/});


console.log("p1.getName(), p1", p1.getName(), p1);
console.log("p2.getName(), p2", p2.getName(), p2);

p1.on("tick", function () {console.log("tick :: this", this);});
p2.on("remove", function () {console.log("remove :: this", this);});

Hint: "meta" mixins also can be easily constructed from smaller ones. Thus enabling the compositions of tailored behavioral sets.

var withFencyAura = function FencyAurMixin (group, fencinessFactor) {
  var auraEnabled = this;

  withAuraBase.call(auraEnabled, group);
  withFencinessFactorAura.call(auraEnabled, fencinessFactor);
};
var withCheatingAura = function CheatingAuraMixin (group, config) {
  var auraEnabled = this;

  withAuraBase.call(auraEnabled, group);
  withCheatingFeaturesAura.call(auraEnabled, config);
};

Note: In practice I mostly end up with configurable mixins, whereas the above given example of composing mixins from other mixins seems to be a rare case.

Smut answered 13/10, 2015 at 19:36 Comment(0)
O
0

Thanks for all the comments on this question. I have learned a lot through follow-up reading and have decided upon the best solution for what I want to do - based on the "Flight Mixin" approach posted by plalx

Mixin.use = function(){

    var i, ApplyMixins = arguments;

    return function(constructor)
    {

        if( typeof constructor !== "function" )
        {

            for( var i = 0; i < ApplyMixins.length; i++ )
            {

                if( ApplyMixins[i].init && typeof ApplyMixins[i].init == "function" )
                {
                    ApplyMixins[i].init.call(constructor);
                }

                copyMembersTo( constructor, ApplyMixins[i].functions || {} );

            }

            return constructor;

        }
        else
        {

            var init = [],
                newPrototype = constructor.prototype;

            var newConstructor = function()
            {

                var self = this;

                init.forEach(function(thisInit){
                    thisInit.call(self, null); 
                });

                constructor.apply(this, arguments);

            };


            Object.setPrototypeOf( newConstructor, constructor );
            Object.defineProperty( newConstructor, "name", { value: constructor.name } );

            newConstructor.prototype = newPrototype;
            newPrototype.constructor = newConstructor;

            for( var i = 0; i < ApplyMixins.length; i++ )
            {

                if( ApplyMixins[i].init && typeof ApplyMixins[i].init == "function" )
                {
                    init.push(ApplyMixins[i].init);
                }

                copyMembersTo( newPrototype, ApplyMixins[i].functions || {} );

            }

            return newConstructor;

        }

    };

};

Which when used, looks like:

var MyConstruct = function(){};
MyConstruct = Mixin.use(mixinA, mixinB)(MyConstruct);

// Or all in one go:
var MyConstruct = Mixin.use(my, mix, ins)(function(){
  // Constructor
});
Ominous answered 28/10, 2015 at 3:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.