Monday, April 20, 2015

Mixins for JavaScript Classes

The same code in several places is the pain. Today I will say a few words about repeated parts of classes. Coders had invented a solution of this problem a long time ago: you can move similar methods and properties to a common parent class or, if you don’t have one, you can use mixins. There are many implementations of this pattern in JavaScript, but I want to review the case when a mixin is placed to the prototype chain.

Picture of Idea

Let’s start with visual presentation of the problem. Consider we have two base classes and two child classes inherited respectively.

At one point of development process, similar functionality is required in both the child classes. A dull copy-paste will look on the scheme like that

It’s very often, the functionality has nothing to do with the base classes, therefore, to move it to another parent class is improperly and inconvenient at least. So let’s place it to a special entity - mixin. In the context of the language mixin can be a simple object.

Now we are about to most interesting question, that is “What’s the right way to mix our mixin to the classes?” From a personal perspective I can assert that the best way is creation of temporary classes with the mixin as a prototype and insertion them into the prototype chains.


Advantages of such an approach:
  • simplicity of implementation;
  • easy to redefine and extend any definition of methods and properties;
  • flexibility of plugging in, ability of writing mixins with dependencies relatively easy;
  • adding one more pattern to the code does not increase its complexity and the cost of support because of using existing mechanisms of inheritance;
  • performance: you need not any loop, only a couple of Object.create
  • highly effective memory usage: you do not copy anything

The Code

A specific implementation is used in all the following examples - Backbone.Mix library. If you look at its code, you will see only two files; each of them is easy and short, so you can write a library for your prefered framework very fast.

Let’s try mixins embedded to the prototype chain in the real life and see the advantages practically. Imagine you are writing a web site )) and there is a number of elements, which can get closed, such as popups, hints etc. All of them have to listen to click events on elements with the `close` CSS class and hide the main element. A mixin for this purpose can be
var Closable = {
    events: function () {
        return {
            'click .close': this._onClickClose
        };
    },

    _onClickClose: function () {
        this.$el.hide();
    }
};
The code below shows how to mix Closable
var Popup = Backbone.View.mix(Closable).extend({
    // write something amazing here!!
});

Easy, right? Let’s look at a prototype chain in this particular case

  • the chain starts with a base class - Backbone.View
  • the second thing in the chain is an anonymous class with Closable as a prototype
  • at the end of the chain we can see our Popup
This concept allows us to extend methods of mixins in classes elegantly, just like in child classes. For example, we can log something to the console after Popup is closed:
var Popup = Backbone.View.mix(Closable).extend({
    _onClickClose: function () {
        this._super();
        console.log('Popup closed');
    }
});
I use the backbone-super library here and next times in code examples.

Order of Mixing

Sometimes Frequently, we need to mix more than one mixin. Imagine, we've got crazy and want to write our logs into the IndexedDB instead of console )) And we have a mixin for this. Greet Loggable!
var Loggable = {
    _log: function () {
        // write to IndexedDB
    }
};
And now we mix two mixins into Popup
var Popup = Backbone.View.mix(Closable, Loggable).extend({
    _onClickClose: function () {
        this._super();
        this._log('Popup closed'); // <- log to IndexedDB
    }
});
Syntax hasn't become harder. Let’s look at the prototype chain.

As you can see order of classes depends on order of mixins passed to mix

Dependent Mixins

The site grows up and our make-believe analyst wants more and more information about how much time users spend on the page, what they prefer for breakfast, what kind of porn they like, and where are they clicking more often. One time he will come and say, “Look, I want to know what they close... We can’t live without that info anymore.” Of course you have already had a mixin for it. And now that you have come to terms with nonsense of analysts, you can start to think about how to bind Closable and Trackable.
var Trackable = {
    _track: function (event) {
        // push the event to some analytic system
    }
};
Obviously, Closable must depend on Trackable as shown on the scheme:

And the desired prototype chain must have Trackable higher than Closable

A code for mixins with dependencies is more complicated, but a bit.
var Closable = new Mixin({
    dependencies: [Trackable]
}, {
    events: function () {
        return {
            'click .close': this._onClickClose
        };
    },

    _onClickClose: function () {
        this.$el.hide();
        this._track('something closed'); // <- a new functionality
    }
});

Now we have the additional option for dependencies; the price is yet another class - Mixin. Closable is not just a simple object any more - it’s an instance of Mixin, but mixing process itself has not changed:
var Popup = Backbone.View.mix(Closable, Loggable).extend({ ... });

Have Your Mixins Documented Properly

WebStorm has excellent support of mixins. It’s enough to write correct JSDoc and hints, autocomplete etc. become more useful. IDE understands @mixin and @mixes tags. Examine following code as an example of sufficient documentation to explain to IDE what the hell is going on.
/**
 * @mixin Closable
 * @mixes Trackable
 * @extends Backbone.View
 */
var Closable = new Mixin({
    dependencies: [Trackable]
}, /**@lends Closable*/{
    /**
     * @returns {object.<function(this: Closable, e: jQuery.Event)>}
     */
    events: function () {
        return {
            'click .close': this._onClickClose
        };
    },

    /**
     * @protected
     */
    _onClickClose: function () {
        this.$el.hide();
        this._track('something closed');
    }
});

/**
 * @class Popup
 * @extends Backbone.View
 * @mixes Closable
 * @mixes Loggable
 */
var Popup = Backbone.View.mix(Closable, Loggable).extend({
    /**
     * @protected
     */
    _onClickClose: function () {
        this._super();
        this._log('Popup closed');
    }
});

Occasionally, mixins are written for classes, which have a specific base class. Closable, which is for classes inherited from Backbone.View, is not an exception. You should define the base class for such the mixins using @extend tag unambiguously to avoid uncomprehending of IDE about where are invocations of certain methods came from. Closable uses $el and events from Backbone.View; let’s look at its docs one more time
/**
 * @mixin Closable
 * @mixes Trackable
 * @extends Backbone.View
 */
var Closable = new Mixin( ... );

I think it’s enough for “a few words about mixis”. Thank you for reading.

Backbone.Mix library on GitHub
More code from the same authors

1 comment: