NodeJS dependency injection – Zander

Standard

Having worked on Java and Spring for almost a decade, my programs are naturally inclined towards using dependency injection. Recently, I started using NodeJS for developing one of our initiative and I felt modularizing my code instead of building one monolithic program. Soon, I realized that in order to keep my modules loosely coupled with each other, I need to use dependency injection and I started searching for framework. While I tumbled across many frameworks, which claims to provide Dependency Injection, none of them really fit to the expectation that I had. Also I came across many posts emphasizing that NodeJS programs do not require separate Dependency Injection frameworks and one can easily tweak require method to achieve desired results. I personally, didn’t like the suggestions and hence I thought of developing one of my own – Zander.

Instead of comparing Zander with other frameworks, I will walk through a simple example and show you how Zander can be quickly configured. I will be using Typescript to illustrate the example. Refer to the following Image class

import {IStorage} from '../storage'
export class Image {
  constructor(private store:IStorage){
  }
  
  public save():void{
    this.store.save("base64 encoded image data.");
  }
  //...Other methods
}

module.exports = Image;

The Image class has method save, which is used to save the contents of the Image to some persistent storage. Since Image class need not be coupled with underlying storage implementation, we are using interface IStore to interact with the implementation.

export interface IStorage{
  save(data:string):void;
}

Above interface declaration defines the contract to be implemented by Storage providers. Lets create a simple file storage module, which stores the data on disk.

import {IStorage} from './';
class FileStorage implements IStorage{
  constructor(){
  }
  
  public save(data:string):void{
    console.log("You are using file storage to save [" + data + "]");
  }
}

module.exports = FileStorage;

While we have our implementations ready, its time to configure these objects for injection. Zander offers multiple approaches including defining all definitions inside one file or splitting across multiple files. The split approach provides you the capability to declare mock objects and use them while unit testing. Lets see the simple configuration (module.json).

{
    "image": {
        "construct": [
            "fileStorage"
        ]
    },
    "fileStorage": {
        "path": "storage/file_storage"
    }
}

Note the logical reference of the File Storage. The name of the file can be different, but injection must use the key used for declaring the configuration. Now to bootstrap all of this configuration, use following code.

import zander = require('zander');
import path = require('path');
var configLoader = new zander.SimpleFilePathMatchLoader(["modules/module.json"]);
var depManager = zander.DependencyInjection({ configLoader: configLoader, modulePath: path.join(__dirname, 'modules') });
depManager.configure().then((complete)=>{
  depManager.getBean("image").save("Sample Image data");
});

Thats all and you are done with configuration. The complete source code is available at github. The benefit of this approach is that, in future if there is need to switch from File storage to cloud storage, a developer has to create a corresponding implementation class. The class should be exported using module.exports and the configuration must be done inside module.json file. None of the dependent classes must be changed nor there is need to tweak any program.

Since the core logic of the class doesn’t require any Zander specific API implementation, the code is cleaner and testable as well.

Be Sociable, Share!

Leave a Reply

Your email address will not be published. Required fields are marked *