Tuesday, September 7, 2010

Inheritance, member attributes and Dependency Injection in JavaScript

I've been working on a real-time collaboration (operational transforms for various types and structures) framework for C# for some time now. A month or so ago, seeking an excuse to give node.js a go, as well as to broaden my appreciation for javascript even further, I decided to attempt a rewrite of my libraries thus far to js.

Along this path, I found myself seeking constructs not completely native to javascript, such as proper inheritance, and eventually dependency injection / ioc containers. John Resig of jQuery fame has written one inheritance base class, aptly named Class (available here), which is based on principles from base2 and Prototype. I've used this base class a lot in the past, so it was a prime candidate for my own additions.

I did make some changes to the John's Class, such as enabling getter / setter only members in a base class. With the original Class framework, given a getter in a base class, calling extend() on this base would cause the getter to be called. In my extend(), getters and setters are looked up and cloned if they are there, rather than cloning their return values. This also means that a setter is not added to a derived class, if a getter was all it had, and vice versa.

Serialization and such

Dealing heavily with serialization, I decided to stick another piece of clutter into the extend method - allowing you to optionally supply a name of the class you're creating. This will in turn be stored in any instance of the class as the string member "__type". Alone, this would be clutter and nothing more, but I also added a few helper objects to the Class framework named 'cast' and 'clone'.

Class.cast(source, type) will take an existing instance and recreate it, keeping all references to its members, as an object which yields true for castRet instanceof type. This is helpful e.g. when dealing with JSON parsed objects, as the deserialized objects - although having the correct members - won't have the prototype, nor constructor, of the type they were serialized from, and thus won't hold up in an instanceof check.

Class.clone(source, typesource) will take an existing instance and do a deep copy of it. The cloned object, and (recursively) all of its cloned child objects, will be cast to the proper type from typesource (e.g. window) - given that they have a __type member. This ensures both a deep copy *and* objects which yield true for instanceof checks.

Dependency injection and attributes

Next on my feature wish list was the ability to provision a hierarchy of objects with root configurable dependencies. In other words, dependency injection (or DI). This pattern essentially rids you of having to knowingly construct and pass around service implementing objects.

In the case of my collaboration framework, hierarchies of elements (e.g. containers with other containers and elements in them) could require a reference to a central event aggregator - to and from which they will publish and subscribe to events, to and from other elements. Doing this without a dependency injection mechanism would mean manually passing references throughout the hierarchy, or instantiating them wherever needed - something I could easily mess up. Dependency injection gives you a single point of configuration for your dependencies, and that's a good thing.

To implement automatic dependency injection, I decided to bring the concept of attribute code to javascript. Not necessarily the prettiest construct you ever saw - or immediately the clearest - but it does gradually get better.

Imagine classes Other, Base and Derived:

var Other = Class.extend({
init: function()
{
this.value = 42;
}
});


var Base = Class.extend({});
var Derived = Base.extend({
init: function() {}
});

I want to express that Derived depends on an instance of type Other, without having to accept, assign and pass it around myself. Given yet another extension I made to the Class framework, namely the ClassAttribute class, implementing a DI container and matching attribute to do this was simple.

The base ClassAttribute has nothing but an 'actualize(instance)' member function. Whenever the Class framework constructor finds one of these ClassAttributes in the prototype of a class it's instantiating, it will assign the instance with the result from calling type.prototype.attribute.actualize(instance). Since the base ClassAttribute's actualize returns null, this means that an instance of the class will never have a member which is an instanceof ClassAttribute - it will either be null, or something you supply from a derived attribute class.

In the case of my wish for expressing dependencies, I wrote a derived DependencyAttribute, the constructor of which accepts a type name, and an actualize which returns an instance set on the DependencyAttribute. Expanding the Derived class above amounts to:

var Derived = Base.extend({
// The following member value (DepAttribute instance) will *never* be part of a Derived instance.
// 'member' will either be null, or an instance of Other (or something derived from Other). The
// DependencyAttribute will be present in the prototype only.
member: new DependencyAttribute("Other"),
init: function() {}
});

A word of caution here, though - with the Class framework it's a bad idea to declare and assign members other than ClassAttribute derived ones in the object you pass to extend. All instances here will be part of the class' prototype, which in turn means that all instances of your class will share the same member instance. Seeing as my extension of Class deals with members which are instances of ClassAttribute, that rule does not apply for their kind.

The final piece to this puzzle, then, is the DI container. At present it's a fairly basic implementation, but it does handle recursive dependency injects and detects cyclic dependencies.

Given that the adapted Class framework processes and actualizes attributes, prior to calling an instance's constructor, implementing what I called CoffeeContainer meant simply accepting instances and types through registerInstance(name, instance) and registerType(name, type) respectively, as well as providing a recursive builder. Using it in an extended example would look like this:

var YetAnother = Class.extend({
init: function()
{
this.yetAnotherValue = "baz";
}
});
var Other = Class.extend({
foo: new DependencyAttribute("YetAnother"),
init: function()
{
this.otherValue = 42;
}
});

var Base = Class.extend({});
var Derived = Base.extend({
bar: new DependencyAttribute("Other"),
init: function() {}
});

var container = new CoffeeContainer(window);
container.registerInstance("YetAnother", new YetAnother());
container.registerType("Other", Other);
container.registerType("Base", Derived)
var obj = container.getBuilder()("Base");

After running this:
  • obj instanceof Derived == true
  • obj.bar instanceof Other == true
  • obj.bar.foo instanceof YetAnother == true
  • Any other object depending on YetAnother would share the instance with obj.bar

Where to go with this

This is hardly a complete container - it just pulls together a few basic pieces and ideas, and seems to be doing its job for demo purposes. For production usage, I'd recommend giving it a thorough read-through, and make sure there aren't too many hidden flaws in its design.

Also, supporting container scopes, lifetimes and such hasn't been considered at all in this example. If you require such: go implement it. Worth noting in that regard is that all builders will share the type repository with previously returned builders. It's simple to deal with this, but for the sake of this example; I haven't bothered.

Source code files

Class.js - originally by John Resig, since updated and extended quite a bit

(function() {
var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
this.Class = function(){};

// Returns a class, derived from Class. A derived class can be further
// derived by calling extend on its typename.
//
// Example:
// var Derived = Class.extend({
// init: function(a, b, c) { this is the constructor },
// someFunc: function() { };
// });
// var FurtherDerived = Class.extend({
// init: function(a, b, c) { this._super(a, b, c); <- calls above constructor },
// someFunc: function() { this._super(); <- calls someFunc in Derived };
// });
//
// (new FurtherDerived(1, 2, 3) instanceof Derived) will be true
Class.extend = function(prop, typeName)
{
var _super = this.prototype;
initializing = true;
var prototype = new this();
initializing = false;
for (var name in prop)
{
if (typeof prop[name] === "function" &&
typeof _super[name] === "function" &&
fnTest.test(prop[name]))
{
prototype[name] = (function(name, fn) {
return function() {
var tmp = this._super;
this._super = _super[name];
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]);
}
else
{
if (typeof prop[name] == "function") prototype[name] = prop[name];
else if (typeof prop.__lookupGetter__ === "function" &&
(typeof prop.__lookupGetter__(name) !== "undefined" ||
typeof prop.__lookupSetter__(name) !== "undefined" ||
typeof _super.__lookupGetter__(name) !== "undefined" ||
typeof _super.__lookupSetter__(name) !== "undefined"))
{
if (typeof (getter = prop.__lookupGetter__(name)) !== "undefined") prototype.__defineGetter__(name, getter);
else if (typeof (getter = _super.__lookupGetter__(name)) !== "undefined") prototype.__defineGetter__(name, getter);
if (typeof (setter = prop.__lookupSetter__(name)) !== "undefined") prototype.__defineSetter__(name, setter);
else if (typeof (setter = _super.__lookupSetter__(name)) !== "undefined") prototype.__defineSetter__(name, setter);
}
else prototype[name] = prop[name];
}
}

function Class()
{
if (!initializing)
{
for (var name in this)
{
if (this[name] instanceof ClassAttribute)
{ this[name] = this[name].actualize(this); }
}
if (typeof typeName !== "undefined")
{ this.__type = typeName; }
else if (typeof _super.__type !== "undefined")
{ this.__type = _super.__type; }
if (this.init)
{
var args = arguments[0] instanceof ClassArgumentArray ?
arguments[0].args : arguments;
this.init.apply(this, args);
}
}
}
Class.prototype = prototype;
Class.constructor = Class;
Class.extend = arguments.callee;
return Class;
};

// Given a deserialized Class-derived object, which no longer yields true for
// (source instanceof type), Class.cast() will recreate it, based on the original
// type, but without calling the constructor, so that (sourceCast instanceof type)
// is true.
Class.cast = function(source, type)
{
var oldInit = type.prototype.init;
type.prototype.init = function() {};
var obj = new type();
type.prototype.init = oldInit;
for (name in type.prototype)
{
if (typeof type.prototype[name] !== "function" &&
((typeof type.prototype.__lookupGetter__ !== "function") ||
(typeof type.prototype.__lookupGetter__(name) === "undefined" &&
typeof type.prototype.__lookupSetter__(name) === "undefined")))
{
if (typeof source[name] === "undefined") return null;
obj[name] = source[name];
}
}
return obj;
}

// Does a deep clone of an object. If object down the tree has a __type member
// variable, its string value will be used on typesource (e.g. window) to resolve
// which class the member should be constructed from.
Class.clone = function(source, typesource)
{
if (typeof source == "undefined") return;
var obj = null;
var type = toString.call(source);
if (type === "[object Array]") obj = [];
else if (type === "[object Object]")
{
if (typeof source.__type !== "undefined" &&
typeof typesource[source.__type] !== "undefined")
{ obj = new typesource[source.__type](); }
else obj = {};
}
else if (type === "[object String]" || type === "[object Number]") return source;
else if (type === "[object Function]") return;
for (var name in source)
{
var value = Class.clone(source[name], typesource);
if (typeof value != "undefined") obj[name] = value;
}
return obj;
}

// Construction helper class, which allows you to pass arguments as an array
// to a Class derived constructor.
//
// Example:
// var foo = Class.extend({
// init: function(a, b, c) {}
// });
// new Foo(1, 2, 3) has the same effect as
// new Foo(new ClassArgumentArray([1, 2, 3]))
ClassArgumentArray = Class.extend({
init: function(args) { this.args = args; }
});

// Base class for Attributes. The attribute instances will be available in the
// prototype only - the value of the attributed members within a class instance
// will be whatever a dervied actualize returns - or null if it falls back to the
// base ClassAttribute
//
// Example:
// var MyAttribute = ClassAttribute.extend({
// actualize: function(instance)
// {
// return 42;
// }
// });
// var foo = Class.extend({
// someMember: new MyAttribute(),
// init: function()
// {
// // this.someMember will be 42
// // this.prototype.someMember will be an instanceof MyAttribute
// }
// });
ClassAttribute = Class.extend({
actualize: function(instance)
{
return null;
}
});
})();

CoffeeContainer.js

var CoffeeContainer = Class.extend({
init: function(owner)
{
this.owner = owner;
this.types = {}
},
registerType: function(typeName, handler)
{
if (typeof this.types[typeName] !== "undefined")
{
throw "CoffeeContainer: duplicate type " + typeName;
}
if (typeof handler !== "function")
{
throw "CoffeeContainer: invalid handler";
}
this.types[typeName] = handler;
},
registerInstance: function(typeName, grinder)
{
if (typeof this.types[typeName] !== "undefined")
{
throw "CoffeeContainer Exception: duplicate type " + typeName;
}
this.types[typeName] = function() { return grinder; }
},
getBuilder: function()
{
var factory = this;
var cyclicHistory = {};
return function(typeName)
{
try
{
if (typeof factory.types[typeName] === "undefined")
{
throw "CoffeeContainer Exception: type " + typeName + " not found";
}
if (typeName in cyclicHistory)
{
throw "CoffeeContainer Exception: cyclic dependency: " + typeName;
}
cyclicHistory[typeName] = 1;
var actualType = factory.types[typeName];
for (var name in actualType.prototype)
{
if (actualType.prototype[name] instanceof DependencyAttribute)
{
actualType.prototype[name].value =
arguments.callee(actualType.prototype[name].type);
}
}
delete cyclicHistory[typeName];
var args = [];
for (var i = 1; i < arguments.length; ++i) args.push(arguments[i]);
return new actualType(new ClassArgumentArray(args));
}
catch(e)
{
cyclicHistory = {};
throw e;
}
}
}
});

var DependencyAttribute = ClassAttribute.extend({
init: function(type)
{
this.type = type;
this.value = null;
},
actualize: function()
{
var value = this.value;
this.value = null;
return value;
}
});