@stone-js/service-container
v0.0.43
Published
Vanilla Javascript IoC Service Container with proposal decorator, proxy resolver and destructuring injection
Downloads
280
Maintainers
Readme
Stone.js: Service Container
IoC Service Container with proposal decorator, proxy resolver and destructuring injection provides a very simple, centralized container that stores and resolves libraries, objects, and values to better organize code, manage dependencies, and enhance testability.
Synopsis
The Service Container is an advanced JavaScript/TypeScript library that helps you manage dependencies effectively in your applications. It utilizes the Inversion of Control (IoC) pattern to simplify dependency injection, allowing you to create highly decoupled and maintainable codebases.
By using Dependency Injection (DI), the service container ensures that objects are provided with their dependencies from an external source, rather than creating them internally. This approach improves testability, minimizes code duplication, and promotes the Single Responsibility Principle by allowing objects to focus only on their primary functions.
In addition to DI, the Service Container implements the Proxy pattern, which lets you access dependencies as properties of the container itself. This feature, combined with destructuring, enables a streamlined approach to dependency injection, making it easy to use dependencies without the need for extra boilerplate code.
With the Service Container, you can easily register, resolve, and manage dependencies, making it suitable for projects of any scale. It supports binding instances, singletons, and factories, as well as providing auto and conditional binding options. This flexibility makes your application more modular, scalable, and maintainable, while ensuring efficient dependency management throughout the development process.
Installation
The Service Container
library is available from the npm registry
and can be installed as follows:
npm i @stone-js/service-container
Yarn:
yarn add @stone-js/service-container
PNPM:
pnpm add @stone-js/service-container
[!NOTE] This package is Pure ESM. If you are unfamiliar with what that means or how to handle it in your project, please refer to
this guide on Pure ESM packages
.
Make sure your project setup is compatible with ESM. This might involve updating your package.json
or using certain bundler configurations.
The Service Container
module can only be imported via ESM import syntax:
import { Container } from '@stone-js/service-container';
Getting Started
To get started with the Service Container
, you need to import the Container
class and create an instance of it. This instance will act as the central registry for all your dependencies in your application.
Let's begin with a simple example to discover the power of the service container by logging a message.
1. Create the Logger
Class
First, create a simple Logger
class to log messages:
// Your Logger class
class Logger {
log(message: string) {
console.log(message);
}
}
2. Create the Class that Provides the Message
Next, create a class A
that provides a message to be logged:
// Your class A
class A {
getMessage(): string {
return 'Hello World!';
}
}
3. Create the Class that Logs the Message Using the Logger
Now, create a class B
that depends on Logger
and A
to log the message:
import { Binding } from '@stone-js/service-container';
// Your class B
class B {
private readonly a: A;
private readonly logger: Logger;
constructor({ a, logger }: Binding<A | Logger>) { // Dependency injection by destructuring
this.a = a as A;
this.logger = logger as Logger;
}
logMessage() {
this.logger.log(this.a.getMessage());
}
}
4. Use the Container to Manage Dependencies
Finally, create an instance of the Container
class and use it to manage these dependencies:
import { Container } from '@stone-js/service-container';
// Create a container instance
const container = new Container();
// Auto-register components
container
.autoBinding(A)
.autoBinding(B)
.autoBinding(Logger)
.alias(A, 'a')
.alias(B, 'b')
.alias(Logger, 'logger');
// Make B instance
const b = container.make(B);
// Call logMessage method
b.logMessage(); // Output: Hello World!
In this guide, we covered how to define and register dependencies using the Container
class, and how to leverage dependency injection through destructuring and the Proxy pattern.
By following these steps, you can effectively manage dependencies in your applications, making them more modular, testable, and maintainable.
Usage
The true power of Stone Service Container lies in its ability to simplify and enhance the management of dependencies within your application. By leveraging the Inversion of Control (IoC) and Proxy patterns, the library offers a highly flexible and intuitive way to bind, resolve, and inject dependencies. Whether you're working with simple instances, complex singletons, or dynamic factories, the Service Container provides a robust foundation for building scalable and maintainable applications.
Binding
You can bind instances, singletons, and factories to the container using various methods provided by the Container
class.
Instance
To bind a single instance or value to the container:
// Bind value
container.instance('config', { apiUrl: 'https://api.example.com' });
// Bind instance
container.instance('ApiUrl', new URL('https://api.example.com'));
Singleton
To bind a resolver function that returns a singleton instance:
// Bind a singleton using a resolver function
container.singleton('logger', () => new Logger());
// Resolve the singleton (same instance every time)
const logger1 = container.make('logger');
const logger2 = container.make('logger');
console.log(logger1 === logger2); // Should output: true
Factory
To bind a resolver function that returns a new instance each time:
// To bind a resolver function that returns a new instance each time
container.binding('userService', () => new UserService());
Auto Binding
The autoBinding
method in the service container simplifies the process of binding dependencies by ensuring they are only bound if not already present in the container. This method can be used implicitly or explicitly, providing flexibility in how dependencies are registered and resolved.
Implicit Auto Binding
Implicit auto binding allows you to automatically register and bind a class to the container using the class's own name. This approach simplifies dependency management by using the class name as the key for resolution.
// Auto binding Logger
container.autoBinding(Logger);
// Resolve Logger (it will be registered implicitly if not found)
const logger = container.make('Logger'); // Prefer to use a string key for clarity.
Explicit Auto Binding
Explicit auto binding allows you to register and bind a class to the container with a specified key, as a singleton or factory, and with optional aliases. This method offers greater flexibility in naming and accessing dependencies.
// Auto binding Logger with specified key and aliases
container.autoBinding('logger', Logger, true, ['logging', 'log']);
// Resolve Logger using different aliases
const logger = container.make('logger');
const loggerByAlias = container.make('logging');
Conditional Binding
You can bind instances, singletons, and factories to the container conditionally using various methods provided by the Container
class.
Instance
To bind a single instance or value to the container, if it is not already bound:
// Bind value conditionally
container.instanceIf('config', { apiUrl: 'https://api.example.com' });
// Bind instance conditionally
container.instanceIf('ApiUrl', new URL('https://api.example.com'));
Singleton
To bind a resolver function that returns a singleton instance, if it is not already bound:
container.singletonIf('logger', () => new Logger());
Factory
To bind a resolver function that returns a new instance each time, if it is not already bound:
container.bindingIf('userService', () => new UserService());
Resolving Dependencies
You can resolve dependencies from the container using the make
method:
const config = container.make('config');
const logger = container.make('logger');
const userService = container.make('userService');
On-Demand Resolving
This method provides a convenient way to resolve a value from the container by its key and return it wrapped in a factory function. This approach is useful when you need to create multiple instances of a dependency on demand.
// Get the logger factory function
const loggerFactory = container.factory('logger');
// Resolve logger on demand
const logger = loggerFactory();
Conditional Resolving
This method checks if the dependency is already bound in the container. If it is, the dependency is resolved. If not, the method binds the dependency and then resolves it. This ensures that dependencies are always available when needed, without redundant bindings.
// Assuming Logger is a class that we want to ensure is bound and resolved
container.resolve('Logger', true);
// Resolve the logger instance
const logger = container.make('Logger');
Using Aliases
Aliases allow you to reference bindings by alternative names.
Setting Aliases
To set an alias for a binding:
// Set an alias for Logger
container.alias(Logger, ['logger', 'logging', 'log']);
Retrieving Aliases
To retrieve a binding by its alias:
const logger = container.make('logging');
Checking Bindings
You can check if a binding or alias exists in the container:
// With bound method
const hasLogger = container.bound(Logger); // true
// Can be used with alias
const hasLoggerAlias = container.bound('logging'); // true
// An alias of bound method
const hasLoggerAliasUsingHas = container.has(Logger); // true
// Check if this alias exists
const isAlias = container.isAlias('logging'); // true
Clearing the Container
To remove all bindings and reset the container:
// Clear all bindings and aliases
container.clear();
With this library, managing dependencies in your application becomes much simpler and more efficient. By adhering to the principles of Inversion of Control and Dependency Injection, you can create more modular, testable, and maintainable code.
Summary
The Stone Service Container simplifies dependency management in your applications, making them more modular, testable, and maintainable. It provides tools for binding instances, singletons, and factories, as well as auto and conditional binding. By leveraging Inversion of Control and dependency injection, the service container ensures efficient, flexible, and scalable management of dependencies, fitting projects of any size.
API documentation
Contributing
See Contributing Guide.