So,
Nathan introduced me to his
TypeScript Tiny-IOC implementation last week. As a .NET dev, using IoC/DI is standard practice and nothing new and was immediately comfortable with the concept.
This library has good intentions, but I think it goes against what TypeScript is all about, which is to have type checking and provide more OO like functionality.
In this type of implementation as each Interface has to be doubled up I feel it will cause unnecessary code. Also, you will need to keep each of the IInterfaceChecker implementations up to date whenever it's referring class changes, resulting in a maintenance nightmare.
The problem is that TypeScript does not generate objects for Interfaces. And that is fair enough, as TypeScript in my view is a developer aid for those who prefer the OO world. At the end of the day, it still generates JavaScript.
My main problem with it is the existence of IInterfaceChecker. This "interface" requires the implementing object to provide the methods they implement and the type name. This to me is an immediate "code smell", so I set about seeing how it could be done without magic strings.
Also, I'm still fairly new to the web world, so I thought it would be a good excuse to have a play. :)
So, in JavaScript world there is no such thing as an interface, so my first point is to not use it in TypeScript, instead create a Class named with I as the prefix for anything we want to act like an interface. Specifying I as a prefix is a convention that has been used quite widely so I hope it would not be too much of a problem.
So, to the code...
Models
So, lets create an IPerson class that has some basic details. Remember, this is a Class and not an Interface so we get the generated JavaScript.
class IPerson {
firstName: string;
lastName: string;
salary: number;
constructor(){
this.firstName = '';
this.lastName = '';
this.salary = 1;
}
fullName() {
return this.firstName + ' ' + this.lastName;
}
}
Next, I created some example implementations. One correct implementation, and another 3 that are in some way invalid.
Our Person class is a true implementation of IPerson
class Person {
firstName: string;
lastName: string;
salary: number;
constructor(){
this.firstName = '';
this.lastName = '';
this.salary = 1;
}
fullName() {
return this.firstName + ' ' + this.lastName;
}
}
Below are 3 other objects. The first does not implement salary. In the second, salary is a string and not a number and the third, the fullname function takes two arguments, whilst the interface does not specify any.
class PersonWithNoSalary {
firstName: string;
lastName: string;
constructor(){
this.firstName = '';
this.lastName = '';
}
fullName() {
return this.firstName + ' ' + this.lastName;
}
}
class PersonWithInvalidSalary {
firstName: string;
lastName: string;
salary: string;
constructor(){
this.firstName = '';
this.lastName = '';
}
fullName() {
return this.firstName + ' ' + this.lastName;
}
}
class PersonWithInvalidFullName {
firstName: string;
lastName: string;
salary: number;
constructor(){
this.firstName = '';
this.lastName = '';
}
fullName(firstname: string, lastname: string) {
return firstname + ' ' + lastname;
}
}
JavaScript
For those that don't have TypeScript, below is the equivalent JavaScript for the models.
var IPerson = (function() {
function IPerson() {
this.firstName = '';
this.lastName = ''
this.salary = 1;
this.fullName = function(){
return this.firstName + ' ' + this.lastName;
}
}
return IPerson;
})();
var Person = (function() {
function Person() {
this.firstName = 'Paul';
this.lastName = 'Sanders';
this.salary = 100000;
this.fullName = function(){
return this.firstName + ' ' + this.lastName;
}
}
return Person;
})();
var PersonWithNoSalary = (function() {
function PersonWithNoSalary() {
this.firstName = 'Paul';
this.lastName = 'Sanders';
this.fullName = function(){
return this.firstName + ' ' + this.lastName;
}
}
return PersonWithNoSalary;
})();
var PersonWithInvalidSalary = (function() {
function PersonWithInvalidSalary() {
this.firstName = 'Paul';
this.lastName = 'Sanders';
this.salary = 'My Salary';
this.fullName = function(){
return this.firstName + ' ' + this.lastName;
}
}
return PersonWithInvalidSalary;
})();
var PersonWithInvalidFullName = (function() {
function PersonWithInvalidFullName() {
this.firstName = 'Paul';
this.lastName = 'Sanders';
this.salary = 100000;
this.fullName = function(firstname, lastname){
return firstname + ' ' + lastname;
}
}
return PersonWithInvalidFullName;
})();
Reflection
Now that I have these, I set about creating a Reflection class to help me validate the implementation or one object, against another. After a bit of playing, googling and borrowing a little code from StackOverflow I came up with the following.
var Reflection = (function () {
function Reflection () {
this.getArgs = function(fn) {
var args = fn.toString ().match (/^\s*function\s+(?:\w*\s*)?\((.*?)\)/);
args = args ? (args[1] ? args[1].trim ().split (/\s*,\s*/) : []) : null;
return args;
};
this.implements = function(obj,interface) {
var self = this;
for(var item in interface)
{
if(typeof interface[item] == 'function' && typeof obj[item] != 'function')
{
return false;
}
else if(typeof interface[item] == 'function' && typeof obj[item] == 'function')
{
var interfaceArgs = self.getArgs(interface[item]);
var objArgs = self.getArgs(obj[item]);
//check arguments
if(interfaceArgs.length != objArgs.length)
{
return false;
}
//as js has no types, can only check names
for(i=0;i < interfaceArgs.length;i++)
{
if(interfaceArgs[i] != objArgs[i])
{
return false;
}
}
}
else if(!obj.hasOwnProperty(item))
{
return false;
}
else if(typeof interface[item] != typeof obj[item])
{
return false;
}
}
return true;
}
this.dump = function(obj, elementId) {
var d = document.getElementById(elementId);
var properties = '';
var methods = '';
var classType = obj.constructor.name;
for(var item in obj)
{
if(typeof obj[item] == 'function')
methods += (item + '(' + this.getArgs(obj[item]) + '), ');
else
properties += (item + ' : ' + typeof obj[item] + ', ');
}
d.innerHTML=d.innerHTML + "<p>Type: " + classType + "<br>Properties: " + properties.substr(0,properties.length-2) + '<br>Methods: ' + methods.substr(0,methods.length-2) + '</p>';
};
}
return Reflection;
})();
The
getArgs function was used from a thread on StackOverflow, which is a nice and simple method to obtain arguments of a function.
The
implements function is the main routine. It finds each property and function on the interface and checks the implementing object to see if has the same property/function and that their types match.
The
dump function is a helper function for me quickly visualize an object properties into an element.
IoC
As I'm still learning JavaScript etc, it is only natural that I wrote my own IoC container. :)
This is a pretty simple one, and does not implement
DI.
After a bit more playing, this is what I came up with.
var IoC = (function () {
function IoC () {
this.__keys__ = new Array();
this.__values__ = new Array();
this.isRegistered = function (interface) {
var t = typeof this.__keys__[interface];
var index = this.__keys__.indexOf(interface.__proto__);
if (index == -1) {
return false;
}
return true;
}
this.register = function(obj,interface) {
if(!this.isRegistered(interface))
{
var r = new Reflection();
if(r.implements(obj, interface))
{
this.__keys__.push(interface.__proto__);
this.__values__.push(obj);
}
else
{
throw "Object '" + obj.constructor.name + "' must implement '" + interface.constructor.name + "'";
}
}
}
this.resolve = function(interface) {
if(this.isRegistered(interface)) {
var index = this.__keys__.indexOf(interface.__proto__);
var template = this.__values__[index];
var obj = Object.create(template);
return obj;
}
else
{
throw "No registration found for '" + interface.constructor.name + "'";
}
}
this.unRegister = function(interface) {
if(this.isRegistered(interface))
{
var index = this.__keys__.indexOf(interface.__proto__);
this.__keys__.splice(index,1);
this.__values__.splice(index,1);
}
}
}
return IoC;
})();
The functions are pretty self explanatory, but the main thing to note is that I'm storing the prototype of the "interface" object as my key. In the
register function I'm using the
Reflection object to valid the implementation. If the object does not implement the interface, then I throw an exception.
So, to test all this I could use some testing framework like Jasmine. But, I've not used this yet, and didn't want to get bogged down in figuring it out. So, I've created a regular html page to do this for me. Maybe a Jasmine implementation should be the next item on my long TODO list...
Again, here is the page to test the functionality.
<!DOCTYPE html>
<html>
<head>
<script src="models.js"></script>
<script src="reflector.js"></script>
<script src="ioc.js"></script>
</head>
<script language="javascript">
function validateImplementations() {
var i = new IPerson();
var validPerson = new Person();
var invalid1 = new PersonWithInvalidFullName();
var invalid2 = new PersonWithInvalidSalary();
var invalid3 = new PersonWithNoSalary();
var r = new Reflection();
var result1 = r.implements(validPerson, i);
var result2 = r.implements(invalid1, i);
var result3 = r.implements(invalid2, i);
var result4 = r.implements(invalid3, i);
r.dump(i,"data");
var e = document.getElementById("data");
e.innerHTML += "<hr />";
r.dump(validManager,"data");
printResult('data', 'Does Person implement MyInterface?', result1);
r.dump(invalid1,"data");
printResult('data', 'Does PersonWithInvalidFullName implement MyInterface?', result2);
r.dump(invalid2,"data");
printResult('data', 'Does PersonWithInvalidSalary implement MyInterface?', result3);
r.dump(invalid3,"data");
printResult('data', 'Does PersonWithNoSalary implement MyInterface?', result4);
}
function printResult(id,question, result) {
var e = document.getElementById(id);
var text = '<p style="font-style: italic"><b>' + question + ' ' + (result ? '<span style="color: green">Yes</span>':'<span style="color: red">No</span>');
text += '</b></p><hr />';
e.innerHTML+=text;
}
function testIoc() {
var ioc = new IoC();
var i = new IPerson();
var personTemplate = new Person();
ioc.register(personTemplate, i);
var text = "";
if(ioc.isRegistered(i))
text += ("Registered '" + personTemplate.constructor.name + "' as implementation for " + i.constructor.name);
else
text += ('Could not register ' + personTemplate.constructor.name + ' as implementation for ' + i.constructor.name);
var person1 = ioc.resolve(i);
text += ("<br>Resolved Person 1 (" + person1.constructor.name + ")");
manager1.firstName = 'Fred';
manager1.lastName = 'Flintstone';
text += ("<br>Person 1 Name: " + person1.fullName());
var person2 = ioc.resolve(i);
text += ("<br><br>Resolved Person 2 (" + person2.constructor.name + ")");
manager2.firstName = 'Wilma';
manager2.lastName = 'Flintstone';
text += ("<br>Person 2 Name: " + person2.fullName() + "<br>");
ioc.unRegister(new IPerson());
if(ioc.isRegistered(i))
text += ("<br>Attempt to unregister " + i.constructor.name + " failed");
else
text += ("<br>Unregistered " + i.constructor.name);
try
{
var person3 = ioc.resolve(i);
}
catch(err)
{
text += ("<br>Attempt to resolve failed: " + err);
}
var e = document.getElementById("iocdata");
try
{
var otherPerson = new PersonWithInvalidSalary();
ioc.register(otherPerson, i);
text += "<br><br>Invalid person correctly registered! :(";
}
catch(err)
{
text += ("<br><br>Attempt to register invalid person failed:<br><div style='color: red'>ERROR : " + err + "</div>");
}
e.innerHTML = text;
}
</script>
<body>
<button onClick="testIoc()">Test Ioc</button>
<div id="iocdata"></div>
<hr />
<button onClick="validateImplementations()">Validate Implementations</button>
<div id="data"></div>
</body>
</html>
As you can see, I've saved the models, reflection and ioc in separate JavaScript files. And yes, the IoC would probably benefit from require.js, again something I've not used yet.
An example of everything is
here.