TypeScript: Implementing a Simple IOC Container for Service Location
October 17, 2012 6 Comments
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.
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.
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).
Also here: http://www.infoq.com/articles/Succeeding-Dependency-Injection
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.
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.
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
reflection.ts
hosted with ❤ by GitHub