views:

376

answers:

3

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 data and members/functions or do you consider this attempt to be overkill?

(Note: I believe strongly that DOM IDs should almost never be hardcoded. And, though I use prototypes for public methods of typical classes, you will notice a public function below created outside of the prototype.)

This note is interesting http://yuiblog.com/blog/2007/06/12/module-pattern/

Ext.ns('Foo.Bar');

/**
 * Foo.Bar.MainToolbar (singleton)
 */
Foo.Bar.MainToolbar = (function()
{
  // Temporary, private class used to create and return an object - a singleton
  var toolbarClass = Ext.extend( Ext.Container,
  {
    /**
     * constructor (public)
     */
    constructor: function( config )
    {
      config = config || {};

      // PRIVATE MEMBER DATA ========================================

      // Need IDs for animation anchors
      var accountId = Ext.id( null, 'ls-accountDiv-');
      var faqId = Ext.id( null, 'ls-faqDiv-');
      var logoutId = Ext.id( null, 'ls-logoutDiv-');
      var loginId = Ext.id( null, 'ls-loginDiv-');
      var rulesId = Ext.id( null, 'ls-rulesDiv-');

      var itemCls =
        'color: white; cursor: pointer; font-weight: bold; ' +
        'font-family:Helvetica,Arial,Verdana,Lucida Sans Unicode,Sans-serif;';


      // PUBLIC METHODS *********************************************

      /**
       * userLogin (public) -
       */
      this.userLogin = function( userName, password )
      {
        // Update title bar
        Ext.fly(accountId).update( userName );
        Ext.fly(loginId).hide(true);
        Ext.fly(logoutId).show(true);
      };

      // PRIVATE METHODS ********************************************

      /**
       * handleFaqClick (private) - handler for click on FAQ
       */
      var handleFaqClick = function( event )
      {
        var dialogMsg = '<div style="text-align: leftblah, blah</div>';

        Ext.Msg.show({
          title: 'FAQ',
          modal: true,
          msg: dialogMsg,
          animEl: faqId,
          buttons: Ext.Msg.OK,
          icon: Ext.MessageBox.QUESTION,
          minWidth: '700'
        });
      };

      /**
       * handleLogoutClick (private) - handler for click on logout
       */
      var handleLogoutClick = function( event )
      {
        Ext.fly(accountId).update('');
        Ext.fly(logoutId).hide(true);
        Ext.fly(loginId).show(true);
      };

      /**
       * handleRulesClick (private) - handler for click on RULES
       */
      var handleRulesClick = function( event )
      {
        var dialogMsg = 
          '<div style="text-align: left;"><br/><b>blah, blah</div>';

        Ext.Msg.show({
          title: 'Rules',
          modal: true,
          msg: dialogMsg,
          animEl: rulesId,
          buttons: Ext.Msg.OK,
          icon: Ext.MessageBox.INFO,
          minWidth: '700'
        });
      };


      // CONSTRUCTOR  ===============================================

      // Some parameters (possibly) offered by the user are ignored
      config.id = Ext.id( null, 'ls-mainToolbar-');
      config.layout = 'absolute';
      config.layoutConfig = {};
      config.height = 38;
      config.width = 968;

      config.items = [
      {
        id: Ext.id( null, 'ls-mainToolbar-'),
        xtype: 'box', x: 25, y: 0, height: 36, 
        autoEl: { tag: 'img', src: './images/top_toolbar.png' }

      },{
        id: Ext.id( null, 'ls-logo-'),
        xtype: 'box',
        x: 70, y: 8, height: 22, width: 200,
        autoEl: { style: itemCls, html: 'Foo Bar' }
      },{
        id: accountId,
        xtype: 'box',
        x: 470, y: 8, height: 22, width: 200,
        autoEl: { style: itemCls + ' text-align: right;', html: ' ' }
      },{
        id: logoutId,
        xtype: 'box', x: 730, y: 8, height: 22, width: 36,
        autoEl: {style: itemCls + ' visibility: hidden;', html: 'logout'},
        listeners:
          { render:
            function( cmp ){
              cmp.getEl().addListener('click', 
                handleLogoutClick.createDelegate(this))
            }.createDelegate(this)
          }
      },{
        id: loginId,
        xtype: 'box', x: 730, y: 8, height: 22, width: 36,
        autoEl: { style: itemCls, html: 'login' },
        listeners:
          { render:
            function( cmp ){
              cmp.getEl().addListener('click',
                Foo.Bar.LoginDialog.show.createDelegate(
                  Foo.Bar.LoginDialog, [Ext.emptyFn]))
            }
          }
      },{
        id: rulesId,
        xtype: 'box', x: 800, y: 8, height: 22, width: 36,
        autoEl: { style: itemCls, html: 'rules'},
        listeners:
          { render:
            function( cmp ){
              cmp.getEl().addListener( 'click', 
                handleRulesClick.createDelegate(this) )
            }.createDelegate(this)
          }
      },{
        id: faqId,
        xtype: 'box', x: 860, y: 8, height: 22, width: 26,
        autoEl: { style: itemCls, html: 'faq'},
        listeners:
          { render:
            function( cmp ){
              cmp.getEl().addListener( 'click', 
                handleFaqClick.createDelegate(this) )
            }.createDelegate(this)
          }
      }];

      toolbarClass.superclass.constructor.apply( this, [config] );

      Foo.Bar.LoginDialog.addListener(
        Foo.Bar.LoginDialog.LOGIN_SUCCESSFUL_EVENT(), 
          this.userLogin.createDelegate(this));
    }
  });

  return new toolbarClass();
})();
A: 

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
Kaze no Koe
+2  A: 

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();
Miguel Ventura
"Taking the care to hide data in JavaScript is usually an overkill" - why? Do you disagree with data hiding and abstraction? Do you work on smaller projects with fewer developers where data hiding is inconvenient? Data hiding is commonly accomplished with closures (see above), of course, but why do so few developers bother?
Upper Stage
@Upper Stage: I don't disagree with data hiding and abstraction as you can see if you read my answer. I call it an overkill when the effort you have to make to do it is more wasteful than the benefit that will come from it. JavaScript doesn't make it easy to hide your data (like other languages where you just write "private") and I'd rather be pragmatic than purist: don't hide because it's pretty, instead hide when it's useful.
Miguel Ventura
Pretty? IMHO abstraction is not about pretty, rather about creating a loosely coupled design, maintainability, creating a class hierarchy wherein I/O is contained inside objects, and reducing data passed between objects. I guess we should agree to disagree on this subject :-)
Upper Stage
Hiding data and abstraction are different things. You can abstract by convention (eg: thou shall not invoke methods that start with "_") whereas hiding requires enforcement (can't use them because you can't reach them). Either way allow me to clarify:Don't hide because someone told you it's right, but rather hide when you feel it's going to be useful and it'll pay off. KISS usually pays off better than over-engineering.
Miguel Ventura
Agree with the above comment.
bmoeskau
A: 

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.

bmoeskau
I disagree that the only reason to hide is to prevent someone from accessing whatever you're hiding. I consider abstraction to be hiding unimportant details for the sake of clarity, so I might hide to convey to you (my fellow programmer) what I consider important (and unimportant) in a class. However, you make an inference to a problem with JS and inheritance: we have only private or public, no protected, so to subclass (ExtJS likes subclassing) we must make methods public. This problem (do you agree?) is why I restricted my question to singletons; the issue is more complicated otherwise.
Upper Stage
You're entitled to your opinion. In my opinion, your code comments add just as much -- if not more -- "clarity" regarding what the code is for than the closure. It's not (only) a matter of clarity, it's a matter of code accessibility (API enforcement). If it's a singleton, then it really doesn't matter since as you say it's not a matter of inheritance (although I see no point in making a singleton for the sole purpose of "clarity").
bmoeskau