TypeScript: Implementing a Simple IOC Container for Service Location

Introduction

Inversion of Control is an object-oriented design pattern that encourages de-coupling of objects, by enforcing a layer of abstraction between object interfaces.

The purpose of this post is to explain how to implement a very simple IOC container using TypeScript, focussing on a Service Locator.  This IOC container can be used both for service location of TypeScript generated classes, and external JavaScript libraries.

Update

I have just setup a github repository for TypScriptTinyIoC : https://github.com/blorkfish/typescript-tiny-ioc

End Goal

Our end goal is to be able to use a very simple IOC for service location as follows:

// registration
var typeScriptTinyIoC = new TypeScriptTinyIOC();
TypeScriptTinyIOC.register(new TestImplementsIDrawable(), new IIDrawable());

// service location
var implementsIDrawable = TypeScriptTinyIOC.resolve(new IIDrawable());
expect(implementsIDrawable.draw()).toEqual("drawn");

Reflection (of sorts)

Unfortunately, JavaScript does not support reflection – which is a pre-requisite for IOC containers.  It does, however allow for querying an object for a specific method.  In their book, “Pro JavaScript Design Patterns”, Ross Harmes and Dustin Diaz explain how:

Consider the following code:

class Greeter {
    start() {
    }
}

class FunctionChecker {
    static implementsFunction(objectToCheck: any, functionName: string): bool {
        return objectToCheck[functionName] != undefined;
    };
}

window.onload = () => {
    var greeter = new Greeter();
    var el = document.getElementById('content');
    el.innerHTML = 'Does greeter implement start() ' +
        FunctionChecker.implementsFunction(greeter, 'start');
};

This very simple function checking algorithm will test whether the instance of greeter implements the function start.

Taking this principle one step further, lets define a TypeScript interface with a class name, and a simple array of method names,

interface IInterfaceChecker {
    className: string;
    methodNames: string[];
}

Then we can define a class that implements this InterfaceChecking interface.

export class IITodoService implements IInterfaceChecker {
    className: string = 'IITodoService';
    methodNames: string[] = ['loadMTodoArray', 'storeMTodoArray'];
};

Static Reflection

I guess that you can think of this mechanism as “static reflection”, or “manual reflection”, because we still need to define the list of method names manually.  There are benefits to this approach, though.

Interface Checking Benefits.

While this very simple mechanism may seem trivial, it’s beauty is in it’s simplicity.  It is easy to implement, and promotes reusability – because classes will have documented sets of methods, and can easily be swapped out for different classes that implement the same functionality.

It also provides us a mechanism of determining (at runtime) whether a class implements the desired functionality – and can also be invaluable when your classes depend on external libraries.  Whenever a new version is available, the library can be checked against your list of required functionality.

TypeScript Interfaces

While TypeScript provides the mechanism for strict compile-time checking of interfaces, at run-time we are still dealing with plain-old JavaScript, so the interface definitions are compiled away.  For this reason, we will define a real TypeScript interface, as well as an InterfaceChecker interface definition for use in our InterfaceChecker.  This can be easily accomplished through a simple naming standard:

A simple naming standard I-name and II-name

For standard TypeScript Interfaces, pre-fix the interface with the letter I (as per C# standards) – and for InterfaceChecker Interface definitions, prefix the class name with a double I :

This is the standard TypeScript interface for a TodoService:

interface ITodoService {
    loadMTodoArray() : any [];
    storeMTodoArray(inArray: any[]) : void;
};

and the InterfaceChecker class:

export class IITodoService implements IInterfaceChecker {
    className: string = 'IITodoService';
    methodNames: string[] = ['loadMTodoArray', 'storeMTodoArray'];
};

Then a class that implements the ITodoService :

export class ToDoService implements ITodoService {
    loadMTodoArray(): any [] {
        // load and return an array of objects
        return [{ id: 5},{ id: 6},{ id: 7}];
    };
    storeMTodoArray(inArray: any[]): void {
        // persist here
    };
}

Interface Checking

Our run-time check to ensure that the TodoService class implements ITodoService is then as follows:

var service = new TodoService();
InterfaceChecker.ensureImplements(service , new IITodoService()); 
// above will throw if not implemented

InterfaceChecker

The full code for our interface checker is as follows:

class InterfaceChecker {
    name: string;
    methods: string[];

    constructor (object: IInterfaceChecker) {
        this.name = object.className;
        this.methods = [];
        var i, len: number;
        for (i = 0, len = object.methodNames.length; i < len ; i++) {
            this.methods.push(object.methodNames[i]);
        };
    }

    static ensureImplements(object: any, targetInterface: InterfaceChecker) {
        var i, len: number;
        for (i = 0, len = targetInterface.methods.length; i < len; i++) {
            var method: string = targetInterface.methods[i];
            if (!object[method] || typeof object[method] !== 'function') {
                throw new Error("Function InterfaceChecker.ensureImplements: ' + '
                    object does not implement the " + targetInterface.name +
                    " interface. Method " + method + " was not found");
            }
        }
    };
    static implementsInterface(object: any, targetInterface: InterfaceChecker) {
        var i, len: number;
        for (i = 0, len = targetInterface.methods.length; i < len; i++) {
            var method: string = targetInterface.methods[i];
            if (!object[method] || typeof object[method] !== 'function') {
                return false;
            }
        }
        return true;
    };
}

Once we have the mechanics of our InterfaceChecker in place, it is very simple to implement an IOC container for service Location:

TypeScriptTinyIOC

class TypeScriptTinyIOC {

    static registeredClasses: any[] = [];

    static register(targetObject: any, interfaceType: IInterfaceChecker) {
        var interfaceToImplement = new InterfaceChecker(interfaceType);

        InterfaceChecker.ensureImplements(targetObject, interfaceToImplement); 
        // will throw if not implemented
        if (InterfaceChecker.implementsInterface(targetObject, 
            interfaceToImplement)) 
        {
            this.registeredClasses[interfaceType.className] = targetObject;
        }
    }

    static resolve(interfaceType: IInterfaceChecker): any {
        var resolvedInterface = this.registeredClasses[interfaceType.className];
        if (resolvedInterface == undefined)
            throw new Error("Cannot find registered class that implements " 
                + " interface: " + interfaceType.className);
        return resolvedInterface;
    }

};

TypeScriptTinyIOC usage:

The very simple IOC container can then be used as follows:

interface IDrawable {
    centerOnPoint();
    zoom();
    draw(): string;
}

class IIDrawable implements IInterfaceChecker {
    className: string = 'IIDrawable';
    methodNames: string[] = ['centerOnPoint', 'zoom', 'draw'];
}

class TestImplementsIDrawable implements IDrawable {
    centerOnPoint() {
    };
    zoom() {
    };
    draw() : string {
        return 'drawn';
    };
}

// registration
var typeScriptTinyIoC = new TypeScriptTinyIOC();
TypeScriptTinyIOC.register(new TestImplementsIDrawable(), new IIDrawable());

// service location
var implementsIDrawable = TypeScriptTinyIOC.resolve(new IIDrawable());
expect(implementsIDrawable.draw()).toEqual("drawn");

Have fun,

– Blorkfish.

In my next blog post, I will be tackling TypeScript AMD modules – understanding how to create and use them, how they help with code organisation, and how to mix standard TypeScript classes with AMD modules.

6 Responses to TypeScript: Implementing a Simple IOC Container for Service Location

  1. Hi, made a simple Interface conversion that makes generating the interface checker easy…

    http://jsfiddle.net/HQK8K/1/

    Simply extend your interface with IComparable and use the simple conversion tool provided.

  2. Matthew Buxton says:

    IoC containers should not be used as Service Locators, this is an anti-pattern and is not the way DI should work. Mark Seemann discusses this quite a bit in his DI book (check out page 157 here: http://www.amazon.co.uk/Dependency-Injection-NET-Mark-Seemann/dp/1935182501).

      • blorkfish says:

        Thank you for your comment.
        I have found the service location pattern very useful and am interested in what Mark Seeman’s comments are on the pro’s and cons of such patterns.
        But I must point out that simply commenting that “this is not the way DI should work” is your personal opinion.
        I think that you are missing the point of the article entirely.
        Mark Seeman himself states that the hardest problem is how to get an instance of an interface.
        JavaScript has not concept of interfaces. You cannot get an instance of an interface in JavaScript.
        But by using TypeScript Interfaces, and a little interface checker – you can simulate this in JavaScript. This is the crux of the article.

  3. Matthew Buxton says:

    My choice of words probably weren’t the best, my point really is that the service locator is a right pain in the in the backside and should be avoided. When using IoC, I prefer to use DI as it comes without all the disadvantages of the service locator. They are mutually exclusive, you either chose one way or the other. I guess I don’t see the point of doing it this way when you can use require and typescript modules to give you the dependencies you require making it much cleaner and less of a maintenance nightmare. You can also use constructor injection it TypeScript passing an interface (and not really caring about how it was constructed) but then of course you come to the question… how do you create instances? Ideally, object graphs should be constructed in the composition root of the application adhering to DI best practices.

  4. JavaScript does support just a tiny bit of reflection, though not enough to help with anything you’re trying to do here – thought I’d post and share this just the same though, since we’re on the topic. You can reflect on function/constructor names and parameter names:


    module reflection {
    var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
    var ARGUMENT_NAMES = /([^\s,]+)/g;
    var CACHE_PROPERTY = "__paramNames";
    /// Given a function, obtain it's name
    export function nameOf(func: Function): string {
    return func instanceof Function ? func["name"] : null;
    }
    /// Given a function, obtain a list of argument names
    export function getParamNames(func: Function): string[] {
    if (!func[CACHE_PROPERTY]) {
    // http://stackoverflow.com/a/9924463/283851
    var fnStr = func.toString().replace(STRIP_COMMENTS, '');
    func[CACHE_PROPERTY] = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARGUMENT_NAMES) || [];
    }
    return func[CACHE_PROPERTY];
    }
    /// Given an arbitrary function, and an argument factory function, dispatch the arbitrary function
    export function dispatch(func: Function, factory: { [name: string]: any });
    /// Given an arbitrary function, and a map of argument names/values, dispatch the arbitrary function
    export function dispatch(func: Function, factory: { (name: string): any });
    export function dispatch(func: Function, factory: any) {
    var params = [];
    getParamNames(func).forEach((name) => params.push(
    factory instanceof Function ? factory(name) : factory[name]));
    return func.apply(null, params);
    }
    }
    // EXAMPLES:
    console.log(reflection.getParamNames((baz, bam) => baz + bam)); // => [ 'baz', 'bam' ]
    console.log(reflection.dispatch((baz, bam) => baz + bam,(name) => name + "!")); // => "baz!bam!"
    console.log(reflection.dispatch((baz, bam) => baz + bam, { baz: "wop!", bam: "bam!" })); // => "baz!bam!"
    console.log("function name: ", reflection.nameOf(function hello() { }));

    view raw

    reflection.ts

    hosted with ❤ by GitHub

Leave a comment