Avoiding Bloat in Widgets

—Monday, December 04 2006

Widgets walk a fine line between abstractions and implementations. Implementation, in this case, is a practical solution chosen to perform a given function. The problems with widgets occur when the widget author walks too far in one direction, or worse, walks an outward spiral covering both directions. Both choices lead to script bloat and complexity thats hard to manage. You no longer have a simple solution for a defined problem, you have a complex solution for a variety of problems. The good news is there are ways to avoid both the bloat and complexity.

Building our SuperBox

We’re gonna build a utterly useless SuperBox, and we realize that it could become quiet popular so we want to widgetize it. The only thing we want the SuperBox to do is to jump around on the screen when someone clicks it. Let’s look at how one might do that in Prototype:

var SuperBox = Class.create();
SuperBox.prototype = {
  initialize: function(element, options) {
    this.element = $(element);
    this.options = Object.extend({
      top:  function(offset) { return Math.round(Math.random() * offset); },
      left: function(offset) { return Math.round(Math.random() * offset); },
    }, options || {});
    this.setObservers();
  },
  
  setObservers: function() {
    Event.observe(this.element, 'click', this.onClick.bindAsEventListener(this));
  },
  
  onClick: function(event) {
    var element = Event.element(event);
    element.setStyle({top: this.options.top(200) +  'px', left: this.options.left(200) + 'px' });
  }
};

It’s probably ironic this could be done in far fewer lines of code, but the idea here is to provide a general level of abstraction that will result in less bloat in the future and make sure we don’t have to hack in additional functionality that ends up making it harder for users who just want the basics.

View Example 1

Get an opinion and keep it

It’s very important that you get an opinion and keep it. Quit trying to please everybody; solve your own problems, if you happen to solve the problems of others thats great, but don’t make their problems yours. There will be obvious bugs that don’t fall under this mantra, but just because we can accept options, doesn’t mean everything must be optional.

Things usually start going wrong about the time you begin reading comments like this: “This is great! It would be nice if you could specify a custom class name for X, or a custom effect for the transition.” This is about the time we start loading up the options hash with garbage and hacking away at our original script. Don’t do it!

Subclass, extend, but don’t hack

There is a growing demand for our widget to be jazzed up with effects and we’re feeling the pressure. There are now rogue scripts floating around that have hacked in this functionality, but those scripts are a lot heavier now thanks to all the additional code that had to go into them in order to get the effects that were in high demand. And the real killer now is that in order to get those effects we now have the overhead of effects libraries like Script.aculo.us, Moo.fx, etc.

This isn’t acceptable. Why? Because by hacking the original script to include effects there is no turning back. We’ve completely removed the lightweight and simple purpose of our original script. There are ways to avoid this extra baggage for those of us who only want the basics. Let’s extend our SuperBox to include effects the right way.

Adding functionality by extending

Prototype’s Object.extend serves many purposes, and is a prime fit for adding additional functionality to our script while retaining it’s original simplicity and weight for those who don’t need the bells and whistles. I’m gonna add this piece of code in another file, and include it below our original script:

Object.extend(SuperBox.prototype, {
  onClick: function(event) {
    var element = Event.element(event);
    new Effect.Move(element, {x: this.options.top(200), y: this.options.left(200), mode: 'absolute'});
  }
});

This solves two problems, it gives the additional functionality to those who want it, and eliminates the overhead of an effects library and additional code for those who don’t want it. For those who want their SuperBox to have effects, they just pop in Scriptaculous’ effects.js and the new effects add-on and they have it. It’s backwards compatible too. Just by including these scripts, all their old code should still work, but instead use the effects.

View Example 2

Adding functionality by subclassing

What if someone wanted the effects, but also wanted the box to change colors as well once it moved. This is where Prototype’s lack of good classical inheritance (mimicking) becomes apparent. We could of course just overwrite the onClick handler again, but we’d be duplicating a lot of code because we’d have to rewrite the effect bits just to add in additional functionality.

If Prototype had a better inheritance mechanism, we could write something like this:

var SuperColorBox = Class.extend(SuperBox, {
  onClick: function(event) {
    this.parent();
    this.element.setStyle({backgroundColor: 'red'});
  }
});

The importance of this might not be all that apparent in a script this size, but when you start working with larger widgets or libraries, you can really see the benefit of classical inheritance. I encourage you to take a look at Alex Arnell’s inheritance.js plugin for Prototype and also Dean’s Base.js. I use both of these scripts on a regular basis when dealing with larger applications. They make my job much easier and my code much cleaner.

You might be wondering why setObservers is in a method by itself? I’ve done this in order to allow me the ability to extend this widget further by adding additional observers without having to hack. In larger widgets, there are numerous observers that have to be setup, and we could use subclassing and aspect-oriented programming (see below) to easily hook into this.

Adding functionality through aspect-oriented programming

I love this technique. It’s a tiny bit of code that adds a ton of power. Imagine having callbacks like before, after and around for every method in your class. It really allows you to hook into classes without having to hack at them. Here is actsAsAspect by beppu (whose website seems to be down atm):

function actsAsAspect(object) {
  object.yield = null;
  object.rv    = { };
  object.before  = function(method, f) {
    var original = eval("this." + method);
    this[method] = function() {
      f.apply(this, arguments);
      return original.apply(this, arguments);
    };
  };
  object.after   = function(method, f) {
    var original = eval("this." + method);
    this[method] = function() {
      this.rv[method] = original.apply(this, arguments);
      return f.apply(this, arguments);
    }
  };
  object.around  = function(method, f) {
    var original = eval("this." + method);
    this[method] = function() {
      this.yield = original;
      return f.apply(this, arguments);
    }
  };
}

Now, what if we wanted to make our SuperBox pulsate while it was moving, we can now use the actsAsAspect function to do just this without having to hack into the original script:

actsAsAspect(SuperBox.prototype);
SuperBox.prototype.before('onClick', function() {
  new Effect.Pulsate(this.element, {duration: 1.0});
});

We use the before callback to setup a new effect before our classes original one is run, in this case, the onClick is the one with the preexisting effect.

View Example 3

Be Smart

While this is a rather convoluted example, the premise is solid. When creating widgets, don’t over abstract, and avoid hacking when possible. Even though we jazzed up our widget in the end, it’s original purpose is still intact and we added no additional overhead to it. Those who want “more cowbell” can get it by plugging-in the additional scripts.