iocify
v0.1.1
Published
Inversion of Control for JS
Downloads
4
Readme
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 classA
's instance so that even if no other objects reference it, the container will keep it in memory. When any other class needs to injectA
, the container will return the exact same instance. Thus, the instance ofA
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 thecontainer
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 aMap
object to correlate a key to aResolver
. 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 withtransient()
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 specifytrue
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