Skip to content

Instantly share code, notes, and snippets.

@datchley
Created January 31, 2011 18:51
Show Gist options
  • Save datchley/804559 to your computer and use it in GitHub Desktop.
Save datchley/804559 to your computer and use it in GitHub Desktop.
An implementation idea for JavaScript interfaces
/**
* @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);
@datchley
Copy link
Author

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!

@appcove
Copy link

appcove commented Feb 1, 2011

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