Created
January 31, 2011 18:51
-
-
Save datchley/804559 to your computer and use it in GitHub Desktop.
An implementation idea for JavaScript interfaces
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @class Interface | |
* @constructor | |
* Allows the application of the interface design pattern, by defining | |
* an interface as a name and set of methods. The methods are given a name | |
* and a number of arguments. Not type information. | |
* | |
* The methods are passed as an array, with one object per method. Each | |
* method is defined as follows in the methods array: | |
* | |
* [ ..., { name: funcName, args: 2 }, ... ] | |
* | |
* name - the name of the function | |
* args - the number of arguments expected by this function | |
* | |
* Ex.: | |
* | |
* var Serialize = new Interface('Serialize', [ | |
* { name: 'freeze', args: 1 }, | |
* { name: 'thaw', args: 1 } | |
* ]); | |
* | |
* @param {String} name the name of the interface | |
* @param {Array} methods an array of method definitions, which are objects | |
* | |
*/ | |
var Interface = function(name, methods) { | |
if (arguments.length != 2) { | |
throw new Error("Interface constructor called with " + arguments.length + "arguments. Expects exactly 2."); | |
} | |
this.name = name; | |
this.methods = []; | |
for (var i = 0, len = methods.length; i < len; i++) { | |
if (typeof methods[i] !== 'object') { | |
throw new Error("Interface constructor expects method definitions as objects."); | |
} | |
this.methods.push(methods[i]); | |
} | |
} | |
/** | |
* Extend an object to implement the given interface. This methods | |
* will use the passed Interface and create a stub method for each | |
* required method in the interface in the object parameter. This | |
* new stub simply throws an error. This should be called immediately | |
* after defining the constructor, and subsequently the needed methods | |
* can be redefined after this call, overwriting the stubs. | |
* | |
* Ex., | |
* var MyInterface = new Interface('MyInterface', ...); | |
* var MyObj = function() { | |
* // ... implements MyInterface ... | |
* } | |
* implements(MyObj, MyInterface); | |
* | |
* Each class extended with an interface is not only given stubs | |
* for the interfaces methods, but also a function that users of | |
* that class can call to verify the existence of an interface | |
* (a class can implement more than one interface). | |
* | |
* obj.hasInterface() can be called simply by passing in | |
* one or more Interface names as strings. It returns true if | |
* the object implements ALL the interfaces passed in; false | |
* otherwise. | |
* | |
* Ex., obj.hasInterface('Serialize', 'Observable'); | |
* | |
* @param {Function} obj - the constructor of the class to implement the interface | |
* @param {Object} interface - the instance for the Interface defined | |
*/ | |
function implements(obj, interface) { | |
var methods = interface.methods; | |
for (var j = 0; j < methods.length; j++) { | |
var name = methods[j].name; | |
var args = []; | |
for (var i = 0; i < methods[j].args; i++) { | |
args.push('a'+i); | |
} | |
// Implement a stub for this function, with args that throws an error when called | |
eval("obj.prototype['"+name+"'] = function("+args.join(',')+"){ throw new Error('Interface [" + interface.name + "]."+name+"() is not implemented'); }"); | |
} | |
// Each Class implementing an interface gets a new method (hasInterface()) | |
// which users of that class can call to check for needed interfaces | |
if (typeof obj.prototype.interface === 'undefined') { | |
obj.prototype.interfaces = []; | |
} | |
obj.prototype.interfaces.push(interface.name); | |
if (typeof obj.prototype['hasInterface'] === 'undefined') { | |
obj.prototype['hasInterface'] = function() { | |
if (arguments.length < 1) { | |
return false; | |
} | |
// Cycle through each requested interface argument | |
for (var index in arguments) { | |
var interface = arguments[index]; | |
if (this.interfaces.indexOf(interface) == -1) { | |
return false; | |
} | |
} | |
return true; | |
} | |
} | |
} | |
//---------------------------------------------------------------------- | |
// EXAMPLE USAGE | |
//---------------------------------------------------------------------- | |
// Interface: Serialize | |
// defines an interface for objects that can serialize | |
// their state. | |
var Serialize = new Interface('Serialize', [ | |
{ name: 'freeze', args: 0 }, | |
{ name: 'thaw', args: 1 } | |
]); | |
// Class: Employee | |
// Object that implements the Serialize interface | |
var Employee = function(config) { | |
config = config || {}; | |
this.name = config.name || 'default'; | |
this.salary = config.salary || 30000; | |
this.addDate = new Date(); | |
} | |
implements(Employee, Serialize); | |
// | |
// Employee Class methods | |
// | |
// Serialize interface implementation | |
// If we don't implement these interface methods, we'll get an exception | |
// the first time we try to use them. | |
Employee.prototype.freeze = function() { | |
var store = "{ name: \"" + this.name + "\", salary: " + this.salary + ", addDate: \"" + this.addDate.toString() + "\" }"; | |
return store; | |
} | |
Employee.prototype.thaw = function(store) { | |
eval("var tmp = " + store); | |
this.name = tmp.name; | |
this.salary = tmp.salary; | |
this.addDate = new Date(tmp.addDate) | |
} | |
// | |
// Test it out | |
// | |
// Create an employee and save it's state | |
var emp = new Employee({ name: 'Bob', salary: 100000 }); | |
console.dir(emp); | |
var saved = emp.freeze(); | |
// Recreate the employee from our saved state | |
var emp2 = new Employee(); | |
emp2.thaw(saved); | |
console.dir(emp2); |
Hey, that's a cool concept. As you know from Extruct, I really enjoy this kind of thing.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is just a first attempt at doing 'interfaces' with javascript. You could arrange this in a number of ways, I'm sure. I know 'implements' and 'interface' are reserved words (likely with post 1.8 implementations), so considering how to wrap this in a class or other namespace might be useful as well. I'm not particularly fond on using evals; but given my current implementation of implements(), I didn't really have another way to create the stub functions given that I would know the method name or number of args initially.
I'm always open to suggestions, so feel free to comment!