monopole
v0.3.0
Published
A versatile dependency injection container with features like value bindings, resolvers, aliases, and support for singleton, transient, and scoped lifetimes.
Downloads
103
Maintainers
Readme
monopole
This library provides a powerful and flexible dependency injection container for Deno applications. It allows you to easily manage your application's dependencies and their lifetimes. The library offers a variety of features, including value bindings, resolvers, aliases, and support for different lifetimes (singleton, transient, and scoped).
Features
- Value bindings (also support async)
- Resolver bindings (also support async)
- Class bindings
- Alias bindings
- Inject decorator for resolving dependencies
- Circular dependency resolution (even self dependency!)
- Support for singleton, transient, and scoped lifetimes
- Module
- Bootstrap
Usage
with Deno
import { createContainer } from "https://deno.land/x/monopole/mod.ts";
const container = createContainer();
with Node.js & Browser
Install
npm install monopole
import { createContainer } from "monopole";
// Usage is as above :-)
Value bindings
Value bindings allow you to bind a value directly to a specific key. This is useful when you want to store configuration values, pre-built instances, or other simple values in the container.
Example:
const container = createContainer();
container.value("message", "hello world!");
container.value("instance", Promise.resolve({ name: "instance" }));
const message = await container.resolve("message");
const instance = await container.resolve("instance");
console.log(message); // "hello world!"
console.log(instance); // { name: "instance" }
Resolver bindings
Resolver bindings allow you to provide a factory function that will be invoked when the dependency is resolved. This is useful when you want to create an instance of a class, build an object or return a value based on runtime information.
Example:
const container = createContainer();
container.resolver("resolver", () => ({ message: "this is resolver" }));
container.resolver("asyncResolver", async () => {
return new Promise((resolve) =>
setTimeout(() => resolve({ message: "this is async resolver" }), 50)
);
});
const resolver = await container.resolve("resolver");
const asyncResolver = await container.resolve("asyncResolver");
console.log(resolver); // { message: "this is resolver" }
console.log(asyncResolver); // { message: "this is async resolver" }
Class bindings
Class bindings allow you to bind a class constructor to a specific key. When the key is resolved, a new instance of the class will be created.
Example:
class BaseClass {
}
class MyClass extends BaseClass {
constructor() {
this.message = "hello world!";
}
}
const container = createContainer();
container.bind(BaseClass, MyClass);
const instance = await container.resolve(BaseClass);
console.log(instance.message); // "hello world!"
Alias bindings
Alias bindings allow you to bind one key to another key, effectively creating an alias for a value in the container.
Example:
const container = createContainer();
container.value("original", "this is the original value");
container.alias("alias", "original");
const original = await container.resolve("original");
const alias = await container.resolve("alias");
console.log(original); // "this is the original value"
console.log(alias); // "this is the original value"
Inject decorator for resolving dependencies
The @Inject
decorator is a convenient way to resolve dependencies and inject
them into a class. This decorator makes it easy to specify which dependencies a
class requires, while the dependency injection library takes care of the
underlying instantiation and management of the dependencies.
In the provided example code, a test demonstrates how to use the @Inject
decorator to resolve a dependency for the Controller
class:
class Connection {
}
class Controller {
@Inject("connection")
public connection!: Connection;
}
container.bind("connection", Connection);
container.bind(Controller);
const controller = await container.resolve(Controller);
controller.connection instanceof Connection; // true
Circular dependency resolution
Circular dependency resolution Circular dependency resolution is a feature of the dependency injection library that allows you to handle cases where two or more classes depend on each other. This feature is useful in scenarios where classes have a mutual relationship, such as parent-child or sibling relationships. The library can resolve these circular dependencies automatically, ensuring that the correct instances are injected into the appropriate classes.
In the example test code provided, a circular dependency is created between the
Parent
and Child
classes. Each class has an @Inject
decorator on a
property, indicating that it should be injected with an instance of the other
class:
const container = createContainer();
class Parent {
@Inject("child")
public child!: Child;
}
class Child {
@Inject("parent")
public parent!: Parent;
}
container.bind("parent", Parent);
container.bind("child", Child);
// assert
const parent = await container.resolve<Parent>("parent");
const child = await container.resolve<Child>("child");
console.log(parent.child === child); // true
console.log(child.parent === parent); // true
Support for singleton, transient, and scoped lifetimes
The container supports different lifetimes for bindings:
- Singleton: The instance will be created once and reused for all subsequent resolutions.
- Transient: A new instance will be created for each resolution.
- Scoped: The instance will be created once per scope.
Example:
class SingletonClass {}
class TransientClass {}
class ScopedClass {}
const container = createContainer();
container.bind(SingletonClass).lifetime(Lifetime.Singleton);
container.bind(TransientClass).lifetime(Lifetime.Transient);
container.bind(ScopedClass).lifetime(Lifetime.Scoped);
// Singleton example
const singleton1 = await container.resolve(SingletonClass);
const singleton2 = await container.resolve(SingletonClass);
console.log(singleton1 === singleton2); // true
// Transient example
const transient1 = await container.resolve(TransientClass);
const transient2 = await container.resolve(TransientClass);
console.log(transient1 === transient2); // false
// Scoped example
const scopedContainer = await container.scope();
const scoped1 = await scopedContainer.resolve(ScopedClass);
const scoped2 = await scopedContainer.resolve(ScopedClass);
console.log(scoped1 === scoped2); // true
Module
Modules offer a convenient way to organize and manage dependencies in your application. By separating concerns, they help make your code more modular and maintainable.
In the following example, a ConnectionModule
is created that provides a
Connection
class and handles connecting and closing the connection during the
boot and close phases of the application lifecycle.
class Connection {
connect(): Promise<void>;
close(): Promise<void>;
}
class ConnectionModule implements Module {
provide(container: ModuleDescriptor) {
container.bind(Connection);
}
async boot(container: ModuleDescriptor) {
const connection = await container.resolve(Connection);
await connection.connect();
}
async close(container: ModuleDescriptor) {
const connection = await container.resolve(Connection);
await connection.close();
}
}
const container = createContainer();
container.register(new ConnectionModule());
await container.boot();
/* ... */
// When the application is shutting down
await container.close();
To use a module, simply create a new instance of it and register it with the
container using the register
method. The module's provide
, boot
, and
close
methods will be called automatically during the container's lifecycle.
Bootstrap
In this guide, we'll explore the boot process in detail and demonstrate how it
enables you to access all objects via get
without promises. We'll also show
how the module's boot method is executed during this process.
Check out the example below:
const container = createContainer();
container.value("value", Promise.resolve("by value"));
container.resolver("resolver", async () => "by resolver");
container.alias("alias.value", "value");
container.alias("alias.resolver", "resolver");
await container.boot();
container.get("value") === "by value"; // true
container.get("resolver") === "by resolver"; // true
container.get("alias.value") === "by value"; // true
container.get("alias.resolver") === "by resolver"; // true
During the execution of the boot method, the following internal steps are performed:
- The registered modules are read and their
provide
methods are executed. - The
boot
method of each registered module is executed. - All registered singleton and scoped objects are created and stored within the container.
This process ensures that all dependencies are properly initialized and available for use throughout your application.
Example
- Deno Web Server Example Set up and run a web application using DI with modular architecture.