Skip to content Skip to sidebar Skip to footer

Singleton Pattern And Abstraction In JS

Although the example below exploits ExtJS, one can easily extrapolate to another framework. I am a fan of abstraction and data hiding (and OO in general); does anyone else hide da

Solution 1:

Taking the care to hide data in JavaScript is usually an overkill, but it might be also a very good idea especially when you're creating a library and want consumers to use the library's public API and not mess around with the internals (lots of imaginative people like that).

In JavaScript the pattern for hiding data/methods is usually creating a closure where you have all your private stuff and let your public API methods with access to that closure.

Simplistic example:

var API = (function() {
    // internal stuff goes in here
    // ...
    // some public methods i'll expose later
    // all have access to the internals since they're inside the closure
    function method1() { ... }
    function method2() { ... }
    var somevar;
    return {
        public_method1: method1,
        public_method2: method2,
        public_var: somevar,
    };
})();

// use the API:
API.public_method1();

Solution 2:

I'll post a fragment of a large project of mine in the hope that it will be useful to you. I am not a professional so you may find things that are silly or badly done. The framework I'm using is Prototype.

Lots of code is missing, I hope you can understand the structure though.

CORE.ELEMENT.BaseInterface is a mixin.

Let me know if you have questions.

function getRandomNumber() {
    return 4; // Chosen by fair dice roll. Guaranteed to be random. Thanks to xkcd.com for this function.
}

/*  -------------------------------
    Core
    -------------------------------
*/

var CORE = function () {
    var mix = function () {
        /* Merge the properties of all the objects given as arguments into the first object, making sure only the first object is modified. Of all the properties with the same name belonging to different objects, the one belonging to the rightmost object wins; that is, precedence goes right to left. */
        var args = $A(arguments);
        if (!args[0]) {
            args[0] = {}; // probably got an empty prototype or something.
        }
        for (var l = args.length, i = 1; i < l; ++i) {
            Object.extend(args[0], args[i]);
        }
        return args[0];
    }

    return {
        mix: mix
    }
}();


var INTERFACE = (function(){

    Notifier = function () {
        CORE.mix(this, {
            max: 5, // max number of notifications shown
            timeout: 8 // a notification disappears after this number of seconds
        }, arguments[0] || {});
        this.elm = ELM.DIV({ // ELM.DIV is too long to explain, it's some trickery I got partly from script.aculo.us - the idea at least.
            attributes:{
                id:'notifier',
                style: 'display: none;'
            }
        })
    };

    CORE.mix(Notifier.prototype, CORE.ELEMENT.BaseInterface, {
        notify: function (msg) {
            if (this.elm) {
                var notes = this.elm.getElementsBySelector('.notification');
                while (notes.length >= this.max) {
                    notes.last().remove();
                }
                this.elm.insert({top: '<div class="notification" style="display: none;">' + msg + '</div>'});
                if (!this.elm.visible()) {
                    this.elm.setStyle('opacity: 0; display: block;');
                    this.elm.morph('opacity: 1;', {
                        duration: 1
                    });
                }
                var newNote = this.elm.down('div');
                newNote.setStyle('opacity: 0; display: block;');
                newNote.morph('opacity: 1;', {duration: 1});
                this.removeNotification.bind(this).delay(this.timeout, newNote);
            }
        },
        removeNotification: function (note) {
            note.remove();
            var notes = this.elm.getElementsBySelector('.notification');
            if (notes.length === 0) {
                this.elm.hide();
            }
        }
    });

    return {
        Notifier: new Notifier() //singleton
    };

})();

/*global Ajax, INTERFACE, CONFIG, CORE, Template $ */

var CONTENT = function () {

    var wallpapers = [];

    wallpapers.toJSON = function () { // needed if I want to send a list of wallpapers back to the server
        var tmp = [];
        this.each(function (wp) {
            if (wp.elm.visible()) {
                tmp.push(wp.toJSON());
            }
        });
        return '[' + tmp.join(', ') + ']';
    };

    var newWallpapers = []; // just a buffer

    Wallpaper = function () {
        CORE.mix(this, {
            thumbUrl: '',
            view: '',
            width: 0,
            height: 0,
            source: ''
        }, arguments[0] || {});
        this.aspect = this.width / this.height;
        switch (this.aspect) {
        case 1.6:
            this.aspect = 2;
            break;
        case 16 / 9:
            this.aspect = 2;
            break;
        case 5 / 4:
            this.aspect = 1;
            break;
        case 4 / 3:
            this.aspect = 1;
            break;
        default:
            if (this.width > 2500) {
                this.aspect = 3;
            } else {
                this.aspect = 0;
            }
        }
        this.dimension = this.width < 1280 ? 0 : (this.width < 1680 ? 1 : (this.width < 1920 ? 2 : 3 ));
        this.hr_aspect = CONFIG.labels.aspect[this.aspect];
        this.hr_source = CONFIG.labels.source[this.source].capitalize();
        this.html = '<div class="source">' + this.hr_source + '</div><a class="thumb" target="_BLANK" href="'+ this.view + '"><img class="thumb" src="' + this.thumbUrl + '" /></a><div class="info"><div class="resolution">' + this.width + 'x' + this.height + '</div><div class="aspect">' + this.hr_aspect + '</div></div>';
    };

    CORE.mix(Wallpaper.prototype, CORE.ELEMENT.BaseInterface, {
        fxParms: null,
        getElement: function () {
            this.elm = document.createElement('div');
            this.elm.className="wallpaper";
            this.elm.innerHTML = this.html;
            return this.elm;
        },
        postInsert: function () {
            if (this.thumbHeight) {
                var x = this.thumbHeight * 200 / this.thumbWidth;
                this.elm.down('img.thumb').setStyle('margin: ' + ((200 - x) / 2) + 'px 0 0;');
            }
            delete this.html;
        },
        toJSON: function () {
            return Object.toJSON({
                thumbUrl: this.thumbUrl,
                view: this.view,
                width: this.width,
                height: this.height,
                source: this.hr_source,
                aspect: this.hr_aspect
            });
        }
    });

    return {
        wallpapers: wallpapers, // wallpapers being shown
        newWallpapers: newWallpapers, // incoming wallpapers
        Wallpaper: Wallpaper // constructor
    };

}();

That's how I make namespaces at the moment. If I don't return something int he last "return" statement, it either survives thanks to a closure or it gets eaten by the garbage collector. Looks like a mess if you're not used to it, I guess. Well, let me know if you find anything in there that is worth asking about.


Just in case it's not evident (it probably isn't); in the return statement at the bottom of a namespace it works like this:

  • If it is capitalized (e.g. Wallpapers: Wallpapers) it's always a constructor
  • If it's a "new ..." statement, it's always a singleton
  • If it's not capitalized (e.g. newWallpapers: newWallpapers) it's either a simple function meant to be called without "new" or a simple object

Solution 3:

If you are writing application code for your own internal use, hiding members in JS isn't all that helpful. The only purpose is to prevent someone from accessing whatever you are hiding, and the only time when that would provide benefit is when you're specifically writing code for use by others and you want to (strongly) enforce an API. Note that even in the case of Ext JS, if you look at most of the classes, the API's are more often than not enforced by convention rather than by closures (private things are merely labeled as private), so that you can override and extend things when needed. There is private stuff when it really should not be messed with, but that's the exception. That's what makes it such a powerful framework -- almost everything is extendable. So it really depends on how rigid you want your end code to be.


Post a Comment for "Singleton Pattern And Abstraction In JS"