splink

JavaScript IoC Container

npm install splink
25 downloads in the last month

Splink — JavaScript Dependency Injection / Inversion of Control

Splink is an attempt to create a useful DI/IoC container for JavaScript that doesn't feel foreign.

Inversion of Control is the practice of making your code more passive to the notion of control by opening it up to external injection of dependencies. The process of binding the components together to form a coherent system is left up to the IoC container at runtime. The ideas behind of Inversion of Control have expanded somewhat from the original definition where it specifically applied to a "don't call us, we'll call you" pattern.

Read more on Wikipedia.

It is my second attempt at such a library for JavaScript and I'm still not entirely convinced it's hitting the target; it's a work-in-progress and an experiment-on-the-run! I'd love to have you along for the ride and provide feedback if you're feeling brave.

Motivation

The core motivation of an IoC container for JavaScript is to encourage modularity and discourage tight coupling of components. IoC is certainly not necessary for this but it provides gentle pressure towards this goal.

As a bonus, you get the following:

  1. Testability: as a result of having more modular and less coupled code, your code is much more testable. It's easier to inject substitute dependencies (mocks, stubs, spies, etc.) into your components and test them in complete isolation to the rest of your system. Being able to test small components in isolation is a huge win for ensuring code quality.
  2. Reusability: with components that are modular and less coupled to the rest of your system, you end up with pieces of code that are much easier to reuse.
  3. Substitutability: with IoC, your components are not reaching out to fetch their dependencies, they leave the injection of dependencies to the IoC container. This makes it much easier to substitute dependencies, either for testing or adjusting functionality. Consider the case of a generic emailer component that leaves its transport up to the IoC container. It becomes a simple exercise of swapping transports in and out, SMTP, local exec, Postmark, Amazon SES, etc. and your component needn't care what it's talking to as long as the interface is consistent.
  4. Pluggability: if your components are designed to accept substitutable external components it becomes easier to allow external influence over what code is injected. Your system and components should end up being easier to adapt by third-parties without needing to modify the actual code.
  5. Complexity reduction: by letting the IoC container decide how to piece your system together then you get to ignore the problems created by circular-dependencies and such. Of course, an IoC container brings a complexity of its own to your project so it's usually not necessary or recommended for smaller codebases; there is a point in the weight of code where the tradeoff makes it worthwhile to delegate responsibility to an IoC container.

Influences & Aims

Splink is somewhat influenced by the Spring Framework, an IoC container for Java (among other things), and by Google Guice, also for Java.

Splink aims to be JavaScript-natural and be relatively small in code size and features with additional features left up to external libraries that may build upon Splink.

See BLUESKY.md for initial ideas of what Splink ought to be, some of which is now implemented.

var Splink = require('splink')

var sp = new Splink()

sp.reg(
    function (words) {
      return this.template
          .replace('{name}', this.name)
          .replace('{words}', words)
    }
  , 'speakFunction'
  , { depends: [ 'template', 'name' ] }
)
sp.reg('{name} says: {words}', 'template')
sp.reg('Frank', 'name')

console.log(sp.byId('speakFunction')('WHAT? SPEAK UP DEAR!'))
// → "Frank says: WHAT? SPEAK UP DEAR!"

Each instance of Splink is a separate container to hold your objects. It can hold any type of JavaScript object, associated with an ID string. You can declare how your objects may depend on other objects within the same container.

In our example above, we've registered 3 objects: a Function and two Strings. The Function object is given the ID 'speakFunction' and we've told Splink that it depends on two more objects with the IDs template and name. The two String objects are then registered with these IDs.

When you fetch an object from Splink using the byId() method, Splink will "wire up" your object with any dependencies before passing it to you. In this case, the default behaviour is to bind the dependencies to the context (this) of 'speakFunction' so it can simply call this.template and this.name to fetch its dependencies.

Registering objects

There are multiple ways to register objects, the most direct is with the .reg() method:

var sp = new Splink()
// register an object with no ID
sp.reg(obj1)

// register an object with ID = 'o2'
sp.reg(obj2, 'o2')

// register an object with ID = 'o3'
// and declare that it depends on object with ID 'foo'
sp.reg(obj3, 'o3', { depends: [ 'foo' ]})

Additionally, you can instantiate Splink with a hash containing simple key:value pairs if you have objects that are simple enough:

var sp = new Splink({ name: 'Frank', template: '{name} says: {words}' })
// our Splink instance already has two registered objects

Registering via the constructor internally simply uses the regAll() method, so it's exactly the same as this (which is more helpful if you have more than one batch of objects to add in a hash):

var sp = new Splink()
sp.regAll({ name: 'Frank', template: '{name} says: {words}' })

Metadata

So far we've covered metadata in the form of the depends option, but we can do more with the 3rd argument to reg(). The metadata we pass to reg() stays with the object in Splink and can even be retrieved in whole later on, we can even store arbitrary data associated with our objects that we may need to fetch from our application; Splink only cares about certain keys.

Metadata can also be directly attached to an object with the $config property. This property can serve the same purpose as the options argument to the reg() method:


var sp = new Splink()
function speakFunction (words) {
  return this.template
      .replace('{name}', this.name)
      .replace('{words}', words)  
}
speakFunction.$config = {
    id: 'speakFunction'
  , depends: [ 'template', 'name' ]
}
sp.reg(speakFunction)

This is particularly useful when distributing your objects across modules in Node.

Metadaata properties

id

Giving an ID to your objects is optional, however if you don't provide an ID you won't be able to fetch it by ID later. IDless objects are useful for fetching objects by category, more on this later.

ID can be provided as the second argument to the .reg() function or it can be provided in the metadata with the key 'id'.

There are currently no rules for the characters you can use for your ID but there may be in the future, try and limit them to the same rules you use for your JavaScript variable names.

depends

The depends key tells Splink what additional objects that our object requires to be wired onto it before giving it back to us later on.

In our 'speakFunction' example above, the main object (the function) is registered with Splink with the depends set to [ 'template', 'name' ]. We're simply saying that we want these objects to be connected to our function at runtime. Generally this means they will be available on this.

You can provide custom local names for your properties by passing in an object for your dependency instead of a string.

sp.reg(
    speakFunction
  , 'speakFunction'
  , { depends: [ { id: 'speakTemplate', as: 'template' }, 'name' ] }
)

In this case, the name object is expected to be in the Splink instance with the ID 'name' and will be wired as this.name, but the template object is expected to be in Splink as 'speakTemplate' and will be wired as this.template.

category

Splink includes a basic way to optionally categorise your objects to fetch later as a group. If you provide a string or an array of strings, Splink will allow it to be fetched with the byCategory() method.

sp.reg(function () { console.log('Me Foo!') }, { category: 'outputters' })
sp.reg(function () { console.log('Me Bar!') }, { category: 'outputters' })
sp.reg(function () { console.log('UG!') },     { category: 'outputters' })

sp.byCategory('outputters').forEach(function (o) { o() })
// →  Me Foo!
//    Me Bar!
//    UG!

See below for more information on how the category property can be used.

type

Generally you don't need to tell Splink what type of object you provide it, it can guess with a simple typeof. However, there's currently one special case (there may be more in the future); if you pass in a function object, Splink doesn't know whether it should be passed back as-is with byId() or whether it should be instantiated before being passed back (i.e. using the new operator.)

Provide type: 'prototype' in your metadata for a function to be instantiated with new when it's fetched from within Splink.

var Splink = require('splink')
var sp = new Splink()

function Speaker () { }
Fred.prototype.speak = function () {
  return this.template
      .replace('{name}', this.name)
      .replace('{words}', words)
}

sp.reg(
    Speaker
  , 'speakFunction'
  , { depends: [ 'template', 'name' ], type: 'prototype' }
)
sp.reg('{name} says: {words}', 'template')
sp.reg('Frank', 'name')

singleton

By default, everything object in a Splink container is a singleton. You'll receive the same instance of an object each time you fetch it with byId() or byCategory(). This means that it only needs to be wired up on the first fetch, after that, fetching is simple & cheap.

By supplying a false value to the singleton property in your metadata, you'll ensure that subsequent calls to byId() and byCategory() will fetch a brand new instance of that object.

There are some important special cases to be aware of however. Whenever you need to bind to a new context (this), you'll end up with a new instance of a function (created with Function#bind()). So, if an object is a function that is a dependency of another object, bound version will be created just for that dependency. Similarly, if you call byId() with a bindTo option (see below for details) then you'll also create a bound copy, regardless of its singleton status.

API

reg(obj[, id][, meta])

Register an object with the given (optional) id and (optional) meta.


regAll(map)

Register objects by a mapping of id to object, in the same way that you can pass in a map/hash to the Splink constructor. See above for details.


byId(id[, options])

Fetch an object with the given id string, if it exists in the Splink instance.

If you supply the bindTo property on your options object, the object will be bound to the context you provided (as this) if you are retrieving a function.


byCategory(category)

Fetch an array of objects tagged with the given category string. See above for details about the category property.


keysByCategory(category)

Fetch an array of ids/keys for objects tagged with the given category string.


meta(id)

Fetch the full metadata object for an object by its id. This is useful if you are using metadata to store additional properties on your metadata for purposes beyond Splink.


scan(obj)

Scan an object an its child objects for registerable objects; that is, objects that have a $config property. In this way you could pass in a collection of objects to be registered in bulk in a less restrictive way than regAll().


scanPathSync(path)

Scan a file and directory (including subdirectories and their files) for objects to register, again searching for objects with the $config property. Any .js files will be passed to require() and the resulting object will be passed to scan() so Splink will search through sub-objects for the $config property.

Because of the deep searching nature of the scan*() methods, care should be taken in what you scan, particularly where objects may have getters defined that execute a function with side-effects when called.

Licence

Splink is Copyright (c) 2012 Rod Vagg @rvagg and licensed under the MIT licence. All rights not explicitly granted in the MIT license are reserved. See the included LICENSE file for more details.

npm loves you