Friday, November 8, 2013

Defining Plugin Architecture for Constructor Libraries

Even in a hypothetical world where objects are packaged into black boxes it would seem that convention and configuration butt heads repeatedly until developers are quite frustrated with their code. In the real world it's even more difficult to package up black boxes because the language architecture design is fundamentally flawed. (I'm looking at you Javascript global namespaces!)

So, I've personally set out to develop quick easy ways to make boxes that appear to be black boxes, but are highly configurable.

Here were some of the goals I had in mind for designing my personal plugin Architecture.

  1. Had to be easy!
  2. Had to be quick!
  3. Plugin Convention over configuration
  4. Boilerplate-able
  5. Should give access to constructor options to determine where the construction of the object goes
  6. Should provide a local scope for hidden variables relative to each object.

Here's a boilerplate you can use to create something like I'm describing.

!function(window, document, undefined){
function constructorObject(options){
//do something with constructor object here
}
window["constructorObject"] = constructorObject;
}(window,document)
view raw gistfile1.js hosted with ❤ by GitHub
I've a feeling we're not in the global namespace anymore.

If some of that code looks confusing to you, I suggest looking at the concept of functional expressions. They give you a "pseudo-blackbox" that can see the global namespace.

If you are good on closures, then we move on to the next step. Actually defining the code to make plugins work. Here's what I personally use:

!function(window, document, undefined){
plugins = [];//Plugin storage object
function constructorObject(options){//object constructor
var self = this; //store itself for later
plugins.forEach(function(plugin){
plugin(self, options);//pass this instance of the constructed object to the plugin
//the options used to construct it are also passed as well
});
}
constructorObject.plugin = function(func){//function to define plugins exposed
plugins.push(func);//append the plugin
};
window["constructorObject"] = constructorObject;//expose the constructor
}(window,document)
view raw gistfile1.js hosted with ❤ by GitHub

This code is rather simplistic, but lets take a look a bit deeper to see the real beauty of it.

plugins.forEach(function(plugin){
plugin(self, options);//pass this instance of the constructed object to the plugin
//the options used to construct it are also passed as well
});
view raw gistfile1.js hosted with ❤ by GitHub

This little function right here is where ALL the magic happens. Let me show you an example of why this is "magic." Writing a plugin is very easy to do here.

!function(constructorObject, undefined){
constructorObject.plugin(function(self, options){
//Plugin constructor code goes here
//Now you have access to the self object and options by reference
if(typeof options !== "undefined" && options.property)//put some logic here...
self.protofunc();//call a function on itself
});
constructorObject.prototype.protofunc = function(){// in the closure space, modify it's prototype
console.log("hello world!");
}
}(constructorObject/*, additional plugin dependencies*/)
view raw gistfile1.js hosted with ❤ by GitHub

Boom! Now you can call this:

var myObject = new constructorObject;
myObject.protofunc();
view raw gistfile1.js hosted with ❤ by GitHub

As if that wasn't enough... let's go back and look at another example. A full on event emitter API.

!function(constructorObject){
constructorObject.plugin(function(self, options){
var events = {};//event parent
self.on = function(eventNameSpace, func){//add the function to the namespace
var event = events[eventNameSpace] = events[eventNameSpace]||[];//coalesce and make sure it's an array
func._function_id = func.name+Date.now();//give it some unique value for later
event.push(func);//store it
return self;//chain!
};
self.trigger = function(eventNameSpace){//activate an event namespace
var event = events[eventNameSpace]||[],
args = [].slice.call(arguments, 1);//grab everything except the namespace and pop it into an array
event.forEach(function(func){//for each event
func.apply(self, args);//apply the arguments
});
return self;//chain!
};
self.off = func(eventNameSpace, func){//remove a queued event
var event = events[eventNameSpace]||[];
for(var i = 0, _len = event.length;i<_len; i++){//for each event
var eventFunc = event[i];//grab the event
if(func._function_id === eventFunc._function_id){//compare id's
event.splice(i,1);//remove it from the queue completely
return self;//all done, don't loop any more
}
}
return self;//Chain!
})
});
}(constructorObject/*, additional plugin dependencies*/)
view raw gistfile1.js hosted with ❤ by GitHub

Take some serious time to learn what just happened here. Each one of the functions created in that script are local to the object itself, because the plugin constructor code is called only once. This allows you to enclose private variables into a black box namespace. It's completely abstracted away to the developer behind the curtain.

Please pay no attention to the variables behind the functional expression.

In functional health,
-Josh

No comments:

Post a Comment