Wednesday, December 24, 2014

Decoupling Dependencies in JavaScript

Dependency Injection

The goal of dependency injection is to decouple objects from their dependencies as much as possible. This means to decouple how the dependencies are selected, but not how they are used.

How a dependency is used depends on abstractions: the client code expects a particular interface and does not care what the dependency object is as long as it provides the interface. This works particularly well in JavaScript where duck-typing is favored over class inheritance.

The solution for decoupling the selection is to move the responsibility of selecting the actual dependency to another piece of code. This decoupling of is the implementation of a principle that Martin Fowler calls "Inversion of Control," and Robert C. Martin titles "Dependency Inversion," the fifth of his SOLID principles of object-oriented development (Fowler, Martin). They have more to say about that than I do! What I will discuss here are the forms that decoupling can take in JavaScript.

Constructor Injection

With "constructor injection" the dependency is received by the client as a parameter to the constructor method, and it is up to the client to assign that value to the property in the object. There is some argument to whether this approach to decoupling is actually "injection." The argument is that since the value is "injected" into the constructor as a parameter but the client has the responsibility to save it as the correct property, is it really injection? If we accept that as true, then everything is really injected everywhere whenever methods are called.

But the really big issue with constructor injection is that the selection of the dependency is tied to the creation of the object. That means that whatever code performs this has two responsibilities: the selection of the client object to create and the selection of what objects it is dependent on. And that clearly violates the goal of the Single Responsibility principal (Martin).

Factory Method

The Factory Method design pattern is another wide-spread solution for decoupling the selection of a dependency (Gamma). In this pattern a method is used to segregate the selection of the dependency to one place. But if the method is a method of the class that uses the dependency as described by the pattern, then nothing has really been decoupled.

Now when the method is moved to a "factory," a class or object that encapsulates the selection as its single responsibility, then decoupling is achieved. Unfortunately while decoupling has been achieved, "injection" has not because the client is clearly requesting the dependency from the factory.

Achieving Dependency Injection

Dependency injection means that the client must be totally unconcerned with the selection of the dependency; it must be "injected" without the client's knowledge and be available when the client needs it. In all existing programming environments the only way to make that work is to have a framework running behind the scenes that injects the dependencies before they are required. Dependency injection has become a main-stream technique for building robust programs in many frameworks, including Spring, Grails, and AngularJS among others.

The following example object is the foundation of a JavaScript framework as a CommonJS module that manages named models and services in order to inject them wherever necessary. This module can easily changed to RequireJS.

A model is a data object and a service is an object that provides functionality through an abstract interface. The framework has two methods: one to register models and the other to register services. Note that to decouple the creation of services and avoid issues with their constructor arguments the framework accepts a reference to an instantiated service object, not the function object used to create the service:

// Injector/main.js
//

(function () {

@tab;require('../ObjectExtensions');

@tab;var Injector = function () {

@tab;@tab;// Initialize the model and service collections.

@tab;@tab;this._models = { };
@tab;@tab;this._services = { };
@tab;};

@tab;Object.assign(Injector.prototype, {

@tab;@tab;_models: null,
@tab;@tab;_services: null,

@tab;@tab;registerModels: function (model) {

@tab;@tab;@tab;for (var p in model) {

@tab;@tab;@tab;@tab;this._models[p] = model[p];
@tab;@tab;@tab;}
@tab;@tab;},

@tab;@tab;registerServices: function (service) {

@tab;@tab;@tab;for (var p in service) {

@tab;@tab;@tab;@tab;this._services[p] = service[p];
@tab;@tab;@tab;}
@tab;@tab;}
@tab;});

@tab;module.exports = Injector;
@tab;exports = module.exports;

}).call(this);

As a best-practice Object.assign is used to mixin the desired prototype properties with any existing prototype, even when the prototype is "empty." This preserves the .constructor property of the prototype. The module ObjectExtensions provides an implementation of Object.assign if it is missing in the JavaScript engine: see my blog post on JavaScript Mixins for more details.

Parameter Injection

Many JavaScript frameworks are heavily dependent on callback functions. The Angular MVC framework is a prime example of this mode, and also happens to be a heavy user of parameter injection when using callbacks. Parameter injection is used to inject the model and service objects as the parameter values when the callback occurs. This is achieved by matching the names and types of the parameters to the models and services.

Parameter injection depends on some type of "reflection" capability in the language. In JavaScript the reflection capability is fulfilled by the toString() method on function objects. toString returns the original text of the function and we can use this to parse the parameter list at runtime. What JavaScript lacks is any way to identify parameter types, so we can only rely on the names.

This new injectParameters method for our framework is the entry point for making a callback. It matches the parameter names to the registered model and service names. The first optional parameter to this method is an activation object for the context of the callback. The first required parameter is the function object that will be invoked as the callback. Parameters that are not matched to models or services are fulfilled in order from whatever additional parameters are passed to this method:

injectParameters: function () {

@tab;var activationObject = {};
@tab;var functionObject;
@tab;var position = 0;

@tab;if (this._isFunctionObject(arguments[0])) {

@tab;@tab;functionObject = arguments[position++];

@tab;} else if (this._isObject(arguments[0])) {

@tab;@tab;@tab;throw new Error('Function object expected');

@tab;@tab;functionObject = arguments[position++];
@tab;}

@tab;// Isolate the parameter list in the function definition and turn it
@tab;// into an array of trimmed strings.

@tab;var match = /\(([^)]*)\)/m.exec(functionObject.toString());
@tab;var parameters = match[1].split(',');

@tab;for (var i = 0; i < parameters.length; i++) {

@tab;@tab;parameters[i] = parameters[i].trim();
@tab;}

@tab;// Match the parameters against the models and services, and assign
@tab;// the unmatched entries in order from args.

@tab;var callParameters = [];

@tab;for (var i = 0; i < parameters.length; i++) {

@tab;@tab;if (typeof this._models[parameters[i]] !== 'undefined') {

@tab;@tab;@tab;callParameters.push(this._models[parameters[i]]);

@tab;@tab;} else if (typeof this._services[parameters[i]] !== 'undefined') {

@tab;@tab;@tab;callParameters.push(this._services[parameters[i]]);

@tab;@tab;} else {

@tab;@tab;@tab;callParameters.push(arguments[position++]);
@tab;@tab;}
@tab;}

@tab;// Call the function with the constructed argument list and
@tab;// return the value.

@tab;return functionObject.apply(activationObject, callParameters);
},

_isFunctionObject: function (object) {

@tab;return Object.prototype.toString.call(object) === '[object Function]';
@tab;// An completly viable alternative is: return typeof object === 'function';
},

_isObject: function (object) {

@tab;return typeof object === 'object';
}

If the first parameter is the function object, then a new empty object is used as the activation object. Note the use of the apply method instead of the call method to execute the function. The call method executes a function with an activation object and a list of arguments. The apply method executes the function with an activation object, and the second parameter is an array of values that will be passed to the function as its parameters.

Property Injection

Another approach is to inject values into properties in an object while it is being created. Unfortunately the new operator cannot be overloaded in JavaScript, so the only way to do this is to delegate the object construction to another function.

There are two modes of injection exemplified by the Spring and Grails frameworks in Java. The Spring framework uses the Java annotation @Autowired to identify which properties should be injected. The Grails framework simply looks for property names that match and assumes that they must be injected. An approximation of the annotation form can be used in JavaScript by preceeding the property assignment in the constructor with an expected comment, such as "// Autowired."

var SomeFunctionObject = function () {

@tab;// Autowired
@tab;this.modelA;

@tab;// Autowired
@tab;this.serviceA

@tab;this.calculatedValue = this.serviceA.doSomethingWith(this.modelA);
};

What follows is a new injectAnnotatedProperties method for our framework. It creates a new object of the constructor function type using Object.create, and then injects the properties before calling the constructor. The constructor uses a contrived syntax to declare to the framework the property to inject: the property is stated as a simple expression following the // Autowired notation. The constructor is executed after the injection, so if the constructor actually assigned a value to the property that would override the injected value:

injectAnnotatedProperties: function (object) {

@tab;if (!this._isFunctionObject(object)) {

@tab;@tab;// This only works with a function object.

@tab;@tab;throw new Error('Function object expected');
@tab;}

@tab;// Isolate instances of "// Autowired this.something = ...;" in
@tab;// the function definition and reduce them to a list of property names.

@tab;var newObject = Object.create(object.prototype);
@tab;var matches = object.toString().match(/\/{2}\s*Autowired\s*this\.([^;]*);/gm);

@tab;if (matches) {

@tab;@tab;@tab;var match = matches[i].match(/this\.([^;]*);/m);

@tab;@tab;@tab;if (this._models[match[1]]) {

@tab;@tab;@tab;@tab;newObject[match[1]] = this._models[match[1]];

@tab;@tab;@tab;} else if (this._services[match[1]]) {

@tab;@tab;@tab;@tab;newObject[match[1]] = this._services[match[1]];
@tab;@tab;@tab;}
@tab;@tab;}
@tab;}

@tab;// Create an arguments array without the function object.

@tab;var newArguments = [];

@tab;for (var i = 1; i < arguments.length; i++) {

@tab;@tab;newArguments.push(arguments[i]);
@tab;}

@tab;// Apply the constructor and return the new object.

@tab;object.apply(newObject, newArguments);
@tab;return newObject;
}


Property Injection and Prototypes

Injection into properties defined in a prototype using "annotations" will not work because the definition of the prototype object is discarded, and therefore the comments are not available. To include the properties of the prototype we have to revert to the Grails form of just blindly injecting the property names that match. Note that we do not actually change the values in the prototype object; we enumerate the properties in the prototype and assign the property values to the object being constructed. This new injectProperties method added to our framework injects over both the constructor function object and the prototype by matching names:

injectProperties: function (object) {

@tab;if (!this._isFunctionObject(object)) {

@tab;@tab;// This only works with a function object.

@tab;@tab;throw new Error('Function object expected');
@tab;}

@tab;// Create a new instance of the object with Object.create.

@tab;var newObject = Object.create(object.prototype);

@tab;// Work off the properties defined in the prototype.

@tab;for (var p in object.prototype) {

@tab;@tab;if (typeof this._models[p] !== 'undefined') {

@tab;@tab;@tab;newObject[p] = this._models[p];

@tab;@tab;} else if (typeof this._services[p] !== 'undefined') {

@tab;@tab;@tab;newObject[p] = this._services[p];
@tab;@tab;}
@tab;}

@tab;// Isolate instances of referenced properties in the constructor: "this.?;"

@tab;var matches = object.toString().match(/this\.[^;]*;/gm);

@tab;if (matches) {

@tab;@tab;@tab;var match = matches[i].match(/this\.([^;]*);/m);

@tab;@tab;@tab;if (typeof this._models[match[1]] !== 'undefined') {

@tab;@tab;@tab;@tab;newObject[match[1]] = this._models[match[1]];

@tab;@tab;@tab;} else if (typeof this._services[match[1]] !== 'undefined') {

@tab;@tab;@tab;@tab;newObject[match[1]] = this._services[match[1]];
@tab;@tab;@tab;}
@tab;@tab;}
@tab;}

@tab;// Create an arguments array without the function object (shift
@tab;// every argument down).

@tab;var newArguments = [];

@tab;for (var i = 1; i < arguments.length; i++) {

@tab;@tab;newArguments.push(arguments[i]);
@tab;}

@tab;// Apply the constructor and return the new object.

@tab;object.apply(newObject, newArguments);
@tab;return newObject;
},

Conclusion

That takes care of all the forms of decoupling: passing the dependency to a constructor, using a factory to decide which dependency is needed, injecting dependencies as parameters during a callback, and injecting dependencies as properties of an object between the object creation and the constructor method.

This framework code implemented as a CommonJS module in Node.js and a full suite of Mocha unit tests is available to download as JsDependencyInjection.zip.


References
See the references page.

No comments:

Post a Comment