Defining and Implementing an Interface in JavaScript
Those of us striving to adhere to object-oriented principles while coding JavaScript are no doubt, familiar with the concept of interfaces, and their benefits. Defining and implementing an interface in an object-oriented language like C# is not terribly difficult; however, JavaScript is a different beast. Some will argue that interfaces aren’t really necessary when writing JavaScript, and it’s true—they aren’t, but does that mean you shouldn’t use them if you could?
For example, what if you had to implement a web map using both Bing and Google? This is the perfect scenario for using an interface—define an interface, and implement two different versions of the interface.
Before we get into the code, let’s be clear that JavaScript is not a type-safe language and there is no way to absolutely enforce adherence to the interface, but we can come pretty close…
First, we define a function (object) called, wait for it…Interface that accepts a single object as a parameter.
var Interface = function(args) {...};
The first thing we want to add to our Interface function is code to ensure that an object is being passed into the function, if not set it to null so we have something to work with later on.
args = args || null;
Next, three local functions are defined as helpers for enforcement of the interface. There is a function to check if an object is of type “undefined”, a function to check if an object is of type “function”, and finally a function to compare the argument count of an interface’s function signature versus the signature of the implementation.
var _isUndefined = function(item) {return typeof item === "undefined";},_isNotFunction = function(item) {return typeof item !== "function";},_hasInvalidArgumentCount = function(signature, implementation) {if(_isNotFunction(signature))throw new Error("\"signature\" parameter should be of type \"function\"");if(_isNotFunction(implementation))throw new Error("\"implementation\" parameter should be of type \"function\"");return signature.length !== implementation.length;};
With that out of the way, we need to do a little validation:
switch (true) {case args === null:throw new Error("No arguments supplied to an instance of Interface constructor.");case _isUndefined(args.type):throw new Error("Interface.type not defined.");case _isUndefined(args.implementation):throw new Error("The interface ".concat(args.type).concat(" has not been implemented."));};
Once initial validation has been taken care of, we can use the args object and begin working on the core functionality by defining three private variables, the last of which will eventually hold and return the implementations of the interface:
var _type = args.type,_implementation = args.implementation;_functions = {};
Now, we need to loop through each property of the args object, look for functions and add those to the private _functions object. Every property of the args object that is a function will become a function within the interface and therefore, will need to be implemented.
for(var item in args) {var signature = args[item];if(!_isNotFunction(signature))_functions[item] = signature;};
At this point, we have the signatures of the interface’s functions and can begin working on the implementation. We are going to loop through the _functions object and within that loop, iterate through each item in the _implementation object, do some comparing and then set the current _functions item to the implementation if the comparison doesn’t throw an error.
for(var signature in _functions) {if(_isUndefined(_implementation[signature]))throw new Error(_type.concat(".").concat(signature).concat(" has not been implemented."));for(var item in _implementation) {var implement = _implementation[item];switch (true) {case _isUndefined(_functions[item]):throw new Error(item.concat(" is not a defined member of ").concat(_type).concat("."));case _isNotFunction(implement):throw new Error(_type.concat(".").concat(item).concat(" has not been implemented as a function." ));case _hasInvalidArgumentCount(_functions[item], implement):throw new Error("The implementation of ".concat(_type).concat(".").concat(item).concat(" does not have the correct number of arguments." ));default:_functions[item] = implement;break;};};};
Finally, we return the implementations.
return _functions;
The hard work is done. At this point, it’s just a matter of defining function that accepts an object holding the implementation and return a new Interface with the proper signatures.
// Define interfacevar iThing = function(implementation) {return new Interface({doStuff: function(param) {},doMoreStuff: function(param1, param2) {},doStuffWithoutParams: function() {},implementation: implementation,type: "iThing"});};// Interface properly implementedvar a = new iThing({doStuff: function(param) {alert("a.doStuff with parameter value ".concat(param));},doMoreStuff: function(param1, param2) {alert("a.doMoreStuff with parameter values ".concat(param1).concat(",").concat(param2));},doStuffWithoutParams: function() {return "a.doStuffWithoutParams";}});a.doStuff("foo"); //alerts "foo"// Throws error// "The implementation of iThing.doMoreStuff does not have the correct number of arguments."var b = new iThing({doStuff: function(param) {alert("b.doStuff with parameter value ".concat(param));},doMoreStuff: function(param1) {alert("b.doMoreStuff with parameter values ".concat(param1).concat(",").concat(param2));},doStuffWithoutParams: function() {return "b.doStuffWithoutParams";}});// Throws error "iThing.doStuffWithoutParams has not been implemented."var c = new iThing({doStuff: function(param) {alert("c.doStuff with parameter value ".concat(param));},doMoreStuff: function(param1, param2) {alert("c.doMoreStuff with parameter values ".concat(param1).concat(",").concat(param2));}});
You can grab the code over at github and have your way with it.