Building a Simple Dependency Injection Container in JavaScript
Dependency Injection (DI) is a powerful design pattern that promotes loose coupling and testability in your code. This challenge asks you to implement a basic DI container in JavaScript, allowing you to register dependencies and resolve them when needed. A well-implemented DI container simplifies managing dependencies and makes your code more modular and maintainable.
Problem Description
You are tasked with creating a JavaScript class called Container that acts as a simple dependency injection container. The container should allow you to:
- Register Dependencies: Register functions or classes as dependencies with a given name (key). The registered value can be a constructor function (class) or a pre-existing instance.
- Resolve Dependencies: Resolve a dependency by its name (key). If the dependency is a constructor function (class), the container should instantiate it using any registered dependencies it requires. If it's a pre-existing instance, it should return that instance directly.
- Handle Missing Dependencies: If a requested dependency is not registered, the container should throw an error.
- Support Dependency Injection via Constructor Arguments: When resolving a class, the container should automatically inject any dependencies that the class constructor expects as arguments. These dependencies must also be registered in the container.
Key Requirements:
- The
Containerclass should have aregistermethod to register dependencies. - The
Containerclass should have aresolvemethod to resolve dependencies. - The container should handle both constructor functions (classes) and pre-existing instances.
- The container should throw an error if a dependency is not found.
- The container should correctly inject dependencies into constructors.
Expected Behavior:
register('logger', MyLogger)registersMyLoggeras a dependency with the key 'logger'.register('api', () => new ApiClient())registers a newApiClientinstance each timeresolve('api')is called.resolve('logger')returns the registered instance ofMyLogger.resolve('api')returns a new instance ofApiClient.resolve('nonExistent')throws an error.- If a class constructor requires dependencies, the container should inject them automatically.
Examples
Example 1:
class MyService {
constructor(logger) {
this.logger = logger;
}
doSomething() {
this.logger.log('Doing something...');
}
}
class MyLogger {
log(message) {
console.log(message);
}
}
const container = new Container();
container.register('logger', MyLogger);
container.register('service', MyService);
const service = container.resolve('service');
service.doSomething(); // Output: Doing something...
Example 2:
const container = new Container();
container.register('api', () => new ApiClient()); // ApiClient is a hypothetical class
const apiClient = container.resolve('api');
console.log(apiClient); // Output: Instance of ApiClient
Example 3: (Edge Case - Missing Dependency)
const container = new Container();
container.register('service', MyService);
try {
container.resolve('missingDependency');
} catch (error) {
console.error(error.message); // Output: Dependency 'missingDependency' not found.
}
Constraints
- The solution must be implemented in JavaScript.
- The
Containerclass should be relatively simple and focused on the core functionality of dependency injection. No need for advanced features like scopes or lifecycle management. - The code should be well-structured and readable.
- The container should handle circular dependencies gracefully (e.g., by throwing an error or preventing infinite recursion). While perfect circular dependency resolution is complex, the container should not crash.
- The maximum number of registered dependencies should be 100. This is to prevent excessive memory usage during testing.
Notes
- Consider using a simple object (e.g., a JavaScript object or a Map) to store the registered dependencies.
- When resolving a class, you'll need to inspect its constructor to determine its dependencies. You can use
constructor.lengthto get the number of arguments the constructor expects. - Think about how to handle different types of dependencies (functions, classes, instances).
- Error handling is crucial. Make sure to throw appropriate errors when dependencies are missing or invalid.
- This is a simplified DI container. Real-world DI containers often have more features and complexity. The goal here is to understand the core concepts.