A running example or working demo of Interface pattern in JavaScript

10,391

Solution 1

I have seen the pattern implemented in different ways, but the idea is simple:

  1. You have some class - your interface - that simply specifies the names of some functions. (You may want to have a class called Interface that your actual interfaces instantiate, just so your interfaces are of type Interface)
  2. You then have some other class that implements such interface. This means that this second class must have at least all of the functions specified by the interface.
  3. Finally, you have some other function somewhere else, that expects to receive an object that implements the interface. In the sample code you mention, this function is addForm, which expects an object that implements the 'Composite' and 'FormItem' interfaces.
  4. This function then loops through all the methods of the interface(s) it expects, and checks that the object you passed in to it also have those methods. If a method from one of the interfaces is not found in the object passed into the function, it determines that object doesn't implement the interface, and throws an exception.

Some people may find this pattern unpractical because of the overhead involved, but given Javascript's lack of native support for interfaces, this is not too bad a solution. Some people may also find that using interfaces for small projects in Javascript is overkill.

Example

var Interface = function(name, methods) {
    this.name = name;
    this.methods = [];

    if (methods.constructor == Array)
        this.methods = methods;
    else if (methods.constructor == String)
        this.methods[0] = methods;
    else
        throw new Error("Interface must define methods as a String or an Array of Strings");
};

var InterfaceHelper  = {
    ensureImplements : function(obj, interfaces) {
       // If interfaces is not an array, assume it's a function pointer
       var toImplement = interfaces.constructor == Array ? interfaces : [interfaces];
       var interface;

       // For every interface that obj must implement:
       for (var i = 0, len = toImplement.length; i < len; i++) {
          interface = toImplement[i];

          // Make sure it indeed is an interface
          if (interface.constructor != Interface)
             throw new Error("Object trying to implement a non-interface. "
             + interface.name + " is not an Interface.");

          // Make sure obj has all of the methods described in the interface
          for (var j = 0, interfaceLen = interface.methods.length; j < interfaceLen; j++)
             if (!obj[interface.methods[j]])
                throw new Error("Interface method not implemented. " 
                + interface.name + " defines method " + interface.methods[j]);
       }

       return true;
    }
};

var Drawable = new Interface("Drawable", ["onDraw"]);

var Surface = function() {
   this.implements = ["Drawable"];

   this.onDraw = function() {
      console.log("Surface Drawing");
   };
};

Usage

var myDrawableSurface = new Surface();

// Returns true
InterfaceHelper.ensureImplements(myDrawableSurface, Drawable);

// Returns false (Error thrown)
InterfaceHelper.ensureImplements(myDrawableSurface, Array);

Solution 2

Completed the book example and here is the working jsfiddle, -

var Interface = function(name, methods) {
    if (arguments.length != 2) {
        throw new Error("Interface constructor called with " + arguments.length + "arguments, but expected exactly 2.");
    }

    this.name = name;
    this.methods = [];

    for (var i = 0, len = methods.length; i < len; i++) {
        if (typeof methods[i] !== 'string') {
            throw new Error("Interface constructor expects method names to be " + "passed in as a string.");
        }

        this.methods.push(methods[i]);
    }
};

// Static class method.
Interface.ensureImplements = function(object) {
    if (arguments.length < 2) {
        throw new Error("Function Interface.ensureImplements called with " + arguments.length + "arguments, but expected at least 2.");
    }

    for (var i = 1, len = arguments.length; i < len; i++) {
        var interface = arguments[i];

        if (interface.constructor !== Interface) {
            throw new Error("Function Interface.ensureImplements expects arguments" + "two and above to be instances of Interface.");
        }

        for (var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) {
            var method = interface.methods[j];

            if (!object[method] || typeof object[method] !== 'function') {
                throw new Error("Function Interface.ensureImplements: object " + "does not implement the " + interface.name + " interface. Method " + method + " was not found.");
            }
        }
    }
};

function Map() {}

Map.prototype.centerOnPoint = function(x,y) {
    alert('center=> x: ' + x + ', y: ' + y);
};

Map.prototype.zoom = function(x){
    alert('zoom : ' + x);
}

Map.prototype.draw = function(){
    alert('draw');
};

var map = new Map();
var DynamicMap = new Interface('DynamicMap', ['centerOnPoint', 'zoom', 'draw']);

function displayRoute(mapInstance) {
    Interface.ensureImplements(mapInstance, DynamicMap);
    mapInstance.centerOnPoint(12, 34);
    mapInstance.zoom(5);
    mapInstance.draw();
}

displayRoute(map);​

Solution 3

ES6 has added syntactical sugar to the language. Below is the ES6 implementation of the same example.

class Interface {
    constructor(name, methods) {
        if (arguments.length < 2) {
            throw new Error('An Interface expects atleast 2 arguments ' + arguments.length
                + ' arguments passed')
            
        }
        this.name = name
        this.methods = []
        methods.forEach(method => {
            if (typeof method !== 'string') {
                throw new Error('Interface expects all the method names to be passed as as a string ' +
                    method + ' is a ' + typeof method)
            }
            this.methods.push(method)
        }, this);
    }

    static ensureImplements(object) {
        if(arguments.length < 2) {
            throw new Error("Function Interface.ensureImplements called with " +
                arguments.length + "arguments, but expected at least 2.")
        }

        for (let i = 1, len=arguments.length; i < len; i++) {
            const interf = arguments[i]
            if(interf.constructor !== Interface) {
                throw new Error('Function expects arguments two or above to be instaces of Interface' )
            }

            for(let j = 0, methodsLen = interf.methods.length; j < methodsLen; j++) {
                const method = interf.methods[j]
                if(!object[method] || !typeof object[method] === 'function') {
                    throw new Error('Does not implement the method the interface' + interf.name + 'Interface.Method '
                    + method + ' not found')
                }
            }
        }
    }
}

const DynamicMap = new Interface('DynamicMap', ['centerOnPoint', 'zoom', 'draw'])

class Map {
    constructor() {
        Interface.ensureImplements(this, DynamicMap)
    }
    centerOnPoint() {
        console.log('Moving to center')
    }
    zoom() {
        console.log('Zooming in')
    }

    draw() {
        console.log('Drawing map')
    }
}

const mapInstance = new Map()

Try playing around with the code by removing the methods in the Map class. Hope it explains better to the people coming from oops background

Share:
10,391
Anmol Saraf
Author by

Anmol Saraf

cool.. ɟɐɹɐs ןoɯuɐ

Updated on June 04, 2022

Comments

  • Anmol Saraf
    Anmol Saraf about 2 years

    I am reading the "pro javascript design patterns" book and finding little difficulty in understanding the "Interface" pattern given in the book chapter 2 as there isn't a complete code example demonstrating the use of this pattern.

    I am looking for some help understanding this pattern with some running code example may be on jsfiddle etc.

    This pattern is explained in the book pages 14 - 22, main point I am not understanding is where and how "addForm" method is called. OR if somebody can complete the ResultFormatter example with some test data and object this will really be very helpful in understanding the pattern.

    Code for the book "Pro Javascript Design Patterns" can be downloaded from http://jsdesignpatterns.com/ and this is Chapter 2.

    Thanks for the help !!