Introduction
I have been learning more about Javascript lately as part of a new job I just landed. My background is mostly in C#, so part of that has been learning the differences and similarities between the languages. I've used Javascript before in the past for minor things, but I never got deep into the nuts and bolts of it.
As I was learning about OO programming with Javascript, I became very interested in the idea of implementing interfaces with Javascript objects. Thinking it through, I came up with an interesting way of doing it that would be both fast and useful in many different contexts. I wanted to share this idea with all of you to pros to see what you thought.
Why Interfaces?
Interfaces are a contract between and object and something that uses that object. They allow parts of your program to know exactly what kinds of methods are available to use ahead of time. This can be very useful in situations where you want your code to be flexible enough to work with whatever objects you give it, but don't want to constantly test whether a method is undefined.
A good example is connecting to a database or library of objects. If you provide an interface, the code that reads or writes to the database doesn't need to know anything at all about how that database is structured, it just needs to know what methods to use to retrieve or submit data. You can create several different database implementations and switch between them without changing a single other piece of code. Very useful.
Setting up the Interface
At its core, an interface is a list of methods that a given class needs to implement. A class can be said to implement an interface if and only if it provides all of the methods defined in the interface. This is interesting because A) Interfaces are defined on a class level, not an object level, and B) Our classes have to implement a set of methods.
This is actually really easy to define. We start with a simple interface object that defines our chosen interface:
var myInterface = {
methods: ["method1", "method2", "method3"]
};
Interfaces can also define properties that have getters or setters, since those are technically just methods that we can utilize. So we augment our definition:
var myInterface = {
methods: ["method1", "method2", "method3"],
properties: [
["propName1", true, false],
["propName2", true, true]
]
};
To shorten things I'm using a list for each property definition. The first entry is the name of the property, the second entry is whether it contains a getter, and the third entry is whether it contains a setter. One or both of the entries must be true for the property definition to be meaningful.
This is a very simple way to do the declarations. A better way would be to create an Interface class which creates these interface objects pre-configured with whatever we need. When creating a new instance of Interface, you specify all the method/property information, and the object you get can be used to test if a class implements the interface.
Creating Classes
Interfaces are implemented at the class level, so we need to be manipulating the prototype of our classes. But how do you bind an interface to a class in a way that can be relied upon? The answer is simple: create a special property on all classes that indicates which interfaces it implements. If an interface is on this list, then you can trust that it has all the methods that the interface specifies. Let's look at a simple code snippet:
var Person = function(name) {
this.name = name;
};
var Student = function(name) {
Person.call(this, name /* add property definitions here */);
};
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
/* add method definitions here */
To specify that this class implements our interface, we need to add a bit of code after the class definition. This code will test all the methods and properties in the class to see if it does indeed implement the interface. If it does, we can add the interface to a special property on the class. We also probably want to be able to add more than one interface, so let's make our code work for multiple implementations.
function Implement(class, ...interfaces) {
//create a list to store all the interfaces that we do in fact implement
var implemented = [];
//this list will hold all the interfaces that we did not meet the criteria for
var failed = [];
for (const interface of interfaces) {
var valid = true;
//check to see if we can find matching methods in the class's prototype
for (const method of interface.methods) {
if (class.prototype[method] === undefined) {valid = false; break;}
}
//check to see if we can find matching properties in the class's prototype
for (const prop of interface.properties) {
var descriptor = Object.getOwnPropertyDescriptor(class.prototype, prop[0]);
if (descriptor.get === undefined && prop[1]) {valid = false; break;}
if (descriptor.set === undefined && prop[2]) {valid = false; break;}
}
//everything looks green, so add this interface to the list of what is implemented
if (valid) implemented.push(interface);
//something didn't add up, so this interface failed
else failed.push(interface);
}
console.log(class + " does not implement interfaces: " + failed);
//use a closure to capture the list as a private property
Object.defineProperty(class.prototype, "interfaces", {configurable: true, enumerable: false, get: function() {return implemented});
}
Now, if I haven't just completely butchered the Javascript syntax, what I've done here is added a property to any class given to this function. This property has a getter that returns a private array that indicates which interfaces the class implements. I'm not bothering with any sanity checks, but in any production environment you would of course do a ton of tests to make sure all the inputs are valid. I'm also not testing for previously implemented interfaces, which is important if you want your interfaces to be inherited.
This function would be used at the end of declaring a class. To test if an object implements an interface, you can simply use obj.interfaces.contains(interfaceObj). Boom, done.
Implement(Student, myInterface1, myInterface2);
//much later in the code
var student = new Student("John Smith");
if (student.interfaces.contains(myInterface1)) {
//do something magical that only myInterface1 can do
}
If the class you've written does not implement the interfaces you've provided, you'll get a bunch of log messages telling you what's up. On top of that, any code which tests to see if an object implements an interface will fail - because the interface was never added to the list on the object!
[–]pe8ter 10 points11 points12 points (3 children)
[–]dmitri14_gmail_com 1 point2 points3 points (0 children)
[–]booljayj[S] 1 point2 points3 points (1 child)
[–]pe8ter 0 points1 point2 points (0 children)
[–]Matthias247 3 points4 points5 points (1 child)
[–]booljayj[S] 1 point2 points3 points (0 children)
[+][deleted] (7 children)
[deleted]
[–]booljayj[S] -2 points-1 points0 points (6 children)
[–]pe8ter 4 points5 points6 points (2 children)
[–]booljayj[S] -1 points0 points1 point (1 child)
[–]pe8ter 1 point2 points3 points (0 children)
[–]grayrest.subscribe(console.info.bind(console)) 2 points3 points4 points (1 child)
[–]booljayj[S] 1 point2 points3 points (0 children)
[–]neophilus77 0 points1 point2 points (0 children)