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.
- Had to be easy!
- Had to be quick!
- Plugin Convention over configuration
- Boilerplate-able
- Should give access to constructor options to determine where the construction of the object goes
- 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) |
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) |
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 | |
}); |
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*/) |
Boom! Now you can call this:
var myObject = new constructorObject; | |
myObject.protofunc(); |
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*/) |
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.
In functional health,
-Josh
No comments:
Post a Comment