iocify

0.1.1 • Public • Published

iocify - dependency injection

A lightweight, extensible dependency injection container for JavaScript powered by Aurelia's dependency-injection and aurelia-property-injection.

This is a bundled version for our needs and is fully compatible with Babel and it's legacy decorators.

Installation

$ npm install iocify --save
 

Note: You may need to install also babel-polyfill.

Injection

Currently we are supporting two types of injection - constructor & property injection.

Constructor injection

 
class Logger {}
class Service {}
 
@inject(Logger, Service)
class App {
  constructor(logger, service) {
    this.logger = logger;
    this.service = service;
  }
}
 
let container = new Container();
let app = container.get(App);
 
 
// app.logger instanceof Logger -> true
// app.service instanceof Service -> true
 

Property injection

class Logger {}
class Service {}
 
class App {
  
  @inject(Logger)
  logger = null;
  
  @inject(Service)
  service = null;
}
 
let container = new Container();
let app = container.get(App);
 
 
// app.logger instanceof Logger -> true
// app.service instanceof Service -> true

Object Lifetime, Child Containers and Default Behavior

Each object created by the dependency injection container has a "lifetime". There are three lifetime behaviors that are typical:

  • Container Singleton - A singleton class, A, is instantiated when it is first needed by the DI container. The container then holds a reference to class A's instance so that even if no other objects reference it, the container will keep it in memory. When any other class needs to inject A, the container will return the exact same instance. Thus, the instance of A has its lifetime connected to the container instance. It will not be garbage collected until the container itself is disposed and no other classes hold a reference to it.
  • Application Singleton - It's possible to have child DI containers created from parent containers. Each of these child containers inherits the services of the parent, but can override them with their own registrations. Every application has a root DI container from which all classes and child containers are created. An application singleton is just like a container singleton, except that the instance is referenced by the root DI container in the application. This means that the root and all child containers will return the same singleton instance, provided that a child container doesn't explicitly override it with its own registration.
  • Transient - Any DI container can create transient instances. These instances are created each time they are needed. The container holds no references to them and always creates a new instance for each request.

Any class can be registered in a container as singleton or transient. What does this process look like? Let's look at a couple of examples to see how things work in practice.

Example 1 - Root Container Resolution

Imagine that we have a single instance of Container called root. If a developer invokes root.get(A) to resolve an instance of A, the root will first check to see if it has a Resolver for A. If one is found, the Resolver is used to get the instance, which is then returned to the developer. If one is not found, the container will auto-register a Resolver for A. This resolver is configured with a singleton lifetime behavior. Immediately after auto-registration, the Resolver is used to get the instance of A which is returned to the developer. Subsequent calls to root.get(A) will now immediately find a Resolver for A which will return the singleton instance.

Example 2 - Child Container Resolution

Now, imagine that we have a Container named root and we call root.createChild() to create a child container named child. Then, we invoke child.get(A) to resolve an instance of A. What will happen? First, child checks for a Resolver for A. If none is found, then it calls get(A) on its parent which is the root container from which it was created. root then checks to see if it has a Resolver. If not, it auto-registers A in root and then immediately calls the Resolver to get an instance of A.

Example 3 - Child Container Resolution with Override

Let's start with an instance of Container named root. We will then call root.createChild() to create a child container named child. Next we will call child.createChild() to create a grandchild container from it named grandchild. Finally, we'll call child.registerSingleton(A, A). What happens when we call grandchild.get(A)? First, grandchild checks for a Resolver. Since it doesn't find one, it delegates to its parent which is the child from which it was created. child then checks for a Resolver. Since child.registerSingleton(A, A) was called on child this means that child will have a Resolver for A. At this point child's resolver is used to get an instance of A which is returned to the developer.

As you can see from these examples, the Container basically walks its hierarchy until it either finds a Resolver or reaches the root. If no Resolver is found in the root, it auto-registers the class as a singleton in the root. This means that all auto-registered classes are application-wide singletons, unless they are overriden by a child container.

Explicit Configuration

For the most part, DI will do what you want with object lifetime. However, you may desire to change the behavior of individual classes for the specific needs of your application. This is easy to do by either directly using the Container API or by decorating your class with a Registration.

The Container Registration API

The usual way to configure a class's lifetime is to use the Container API directly. Typically, you will want to do this configuration up-front in your application's main configure method.

Here's a survey of the registration APIs you have available through a Container instance:

  • container.registerSingleton(key: any, fn?: Function): void - This method allows you to register a class as a singleton. This is the default, as discussed above, so there's rarely a reason to call this method. It is provided in the API for completeness. When calling, provide the key that will be used to look up the singleton and the class which should be used. It's common for the key and class to be the same. If they are the same, then only the key needs to be provided. Here are some examples:
    • container.registerSingleton(History, BrowserHistory);
    • container.registerSingleton(HttpClient);
  • container.registerTransient(key: any, fn?: Function): void - This method allows you to register a class as transient. This means that every time the container is asked for the key, it will return a brand new instance of the class. As with the singleton behavior, the key is requried but the class is optional. If left off, the key will be treated as the class to be instantiated. Here's an example of using transient registration:
    • container.registerTransient(LinkHandler, DefaultLinkHandler);
  • container.registerInstance(key: any, instance?: any): void - If you already have an existing instance, you can add that to the container with this method. You just need to pick a key that the instance will be retrievable by. If no key is provided then the key becomes the instance.
  • container.registerHandler(key: any, handler: (container?: Container, key?: any, resolver?: Resolver) => any): void - In addition to simply declaring behaviors, you can also provide a custom function (a handler) that will respond any time the container is queried for the key. This custom handler has access to the container instance, the key and the internal resolver which stores the handler. This enables just about any sort of custom lifetime to be implemented by supplying a custom function. Here's an example:
    • container.registerHandler('Foo', () => new Bar());

Info: Registration Keys All registration APIs take a key. This key is typically the class itself (for convenience). However, the key can be any type, including strings and objects. This is possible because DI implementation uses a Map object to correlate a key to a Resolver. When using class-oriented registration APIs, if the key is not a class, you must provide the class to be created as the second argument to the API call.

Registration Decorators

As an alternative to explicitly registering types with the container, you can rely on auto-registration, but specify the auto-registration behavior you desire, overriding the default container-root-singleton behavior. To provide auto-registration behavior, you simply decorate your type with an auto-registration decorator. What follows is a basic explanation of built-in registration decorators:

  • transient() - Simply decorate your class with transient() and when it's requested from the container, a new instance will be created for each request.
  • singleton(overrideChild?:boolean) - Normally, types are auto-registered as singletons in the root container. So, why do we provide this decorator? This decorator allows you to specify true as an argument to indicate that the singleton should be registered not in the root container, but in the immediate container to which the initial request was issued.

Warning: Registration Decorator Usage At present, the Decorators spec allows for decorators to use parens or not depending on whether or not the decorator requires arguments. This means that decorator invocation is dependent on how the decorator was implemented internally, which can be confusing from time to time. As a result of the way that the registration decorators are implemented, you must use them with parens.

Resolvers

As mentioned above, the DI container uses Resolvers internally to provide all instances. When explicitly configuring the container, you are actually specifying what Resolver should be associated with a particular lookup key.

You can define the type of Resolver via the following decorators:

  • injct(key)
  • lazy(key)
  • optional(key)

inject(key)

Inject the dependency from the container.

lazy(key)

Specifies the dependency should be lazy loaded. It will inject a method that returns the desired instance.

class Logger {}
 
class App {
  @lazy(Logger)
  getLogger = null;
}
 
let container = new Container();
let app = container.get(App);
 
// app.getLogger() instanceof Logger -> true

optional(key)

Specifies the dependency as optional. Tries to find the desired instance in your container and returns it when found. Otherwise null will be used as the value.

class Logger {}
class Service {}
 
class App {
  
  @optional(Logger)
  logger = null;
  
  @optional(Service)
  service = null;
}
 
let container = new Container();
constainer.registerSingleton(Logger);
let app = container.get(App);
 
 
// app.logger instanceof Logger -> true
// app.service === null -> true

License

MIT

/iocify/

    Package Sidebar

    Install

    npm i iocify

    Weekly Downloads

    2

    Version

    0.1.1

    License

    MIT

    Last publish

    Collaborators

    • zinserjan