JavaScript Dependency Injection Container
Building a robust application often involves managing dependencies between different components. A dependency injection (DI) container automates this process, making your code more modular, testable, and maintainable. This challenge asks you to create a basic DI container in JavaScript that can register services and resolve them with their dependencies.
Problem Description
Your task is to implement a JavaScript class named Container that acts as a dependency injection container. This container should allow you to:
- Register Services: Define how to create instances of your services.
- Resolve Services: Get instances of registered services, automatically injecting their dependencies.
Key Requirements:
Container.register(name, definition):name(string): A unique identifier for the service.definition(function): A function that returns an instance of the service. This function can optionally accept an array of dependency names to be injected.- If a service with the same
nameis already registered, it should throw an error.
Container.resolve(name):name(string): The name of the service to resolve.- Should return an instance of the registered service.
- If the requested service is not registered, it should throw an error.
- Dependencies specified in the
definitionfunction should be resolved recursively by callingContainer.resolve()for each dependency name. - If a dependency cannot be resolved (i.e., it's not registered), it should throw an error.
- Singleton Behavior (Optional but Recommended): For simplicity in this initial challenge, each
resolvecall should create a new instance of the service. However, consider how you might modify this to cache and reuse instances (singleton pattern). For this challenge, we will aim for non-singleton behavior initially.
Expected Behavior:
When resolve(name) is called, the container should:
- Look up the
definitionfor the givenname. - If the
definitionfunction expects arguments (dependencies), it should recursively callresolve()for each dependency name. - Invoke the
definitionfunction with the resolved dependencies as arguments. - Return the result of the
definitionfunction.
Examples
Example 1: Basic Service Registration and Resolution
// Service definitions
class Logger {
log(message) {
console.log(`[LOG] ${message}`);
}
}
class UserService {
constructor(logger) {
this.logger = logger;
}
getUser(id) {
this.logger.log(`Fetching user with ID: ${id}`);
return { id: id, name: "John Doe" };
}
}
// Container setup
const container = new Container();
// Register services
container.register('logger', () => new Logger());
container.register('userService', (deps) => {
const logger = deps[0]; // Assuming logger is the first dependency
return new UserService(logger);
});
// Resolve service
const userService = container.resolve('userService');
const user = userService.getUser(123);
// Expected Console Output: [LOG] Fetching user with ID: 123
// Expected 'user' value: { id: 123, name: "John Doe" }
Example 2: Service with Multiple Dependencies
class Database {
query(sql) {
console.log(`Executing SQL: ${sql}`);
return [{ id: 1, name: "Product A" }];
}
}
class ProductService {
constructor(db, logger) {
this.db = db;
this.logger = logger;
}
getProducts() {
this.logger.log("Fetching all products...");
return this.db.query("SELECT * FROM products");
}
}
// Container setup (assuming Logger and Database are already registered as in Example 1)
const container = new Container();
container.register('logger', () => new Logger());
container.register('database', () => new Database());
container.register('productService', (deps) => {
const db = deps[0];
const logger = deps[1];
return new ProductService(db, logger);
});
// Resolve service
const productService = container.resolve('productService');
const products = productService.getProducts();
// Expected Console Output:
// [LOG] Fetching all products...
// Executing SQL: SELECT * FROM products
// Expected 'products' value: [{ id: 1, name: "Product A" }]
Example 3: Circular Dependency (Should Fail)
// Container setup
const container = new Container();
// Register services with circular dependency
container.register('serviceA', (deps) => {
const serviceB = deps[0];
return { name: 'ServiceA', useB: () => serviceB.doSomething() };
});
container.register('serviceB', (deps) => {
const serviceA = deps[0];
return { name: 'ServiceB', doSomething: () => `ServiceB using ${serviceA.name}` };
});
// Attempt to resolve, expecting an error
try {
container.resolve('serviceA');
} catch (e) {
console.error(e.message); // Expected: Error indicating circular dependency or resolution failure
}
Constraints
- Service names (
name) will be strings. - Service definitions (
definition) will be functions. - The
definitionfunction will receive an array of resolved dependencies in the order they are declared in thedepsarray. - Dependency names within the
definitionfunction'sdepsparameter will be strings. - The container should handle up to a reasonable depth of nested dependencies without causing stack overflow issues during resolution (e.g., 10-20 levels is usually sufficient for typical applications).
- The container must throw specific
Errorobjects for:- Registering a service with an existing name.
- Resolving an unregistered service.
- Failing to resolve a dependency (including circular dependencies).
Notes
- Consider how you will store registered services and their definitions. A JavaScript
Mapor object would be suitable. - The
definitionfunction's signature for dependencies might be implicit in this challenge (e.g., assumingdeps[0]is the first dependency). In a more advanced DI system, you might use named parameters or explicit dependency declaration. For this challenge, rely on the order of thedepsarray passed to the definition function. - Error handling is crucial. Make sure to provide informative error messages.
- For example 3, detecting circular dependencies can be tricky. A common approach is to keep track of the current resolution path. If a service is encountered again in the same path, a circular dependency is detected.