Hone logo
Hone
Problems

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:

  1. Register Services: Define how to create instances of your services.
  2. 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 name is 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 definition function should be resolved recursively by calling Container.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 resolve call 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:

  1. Look up the definition for the given name.
  2. If the definition function expects arguments (dependencies), it should recursively call resolve() for each dependency name.
  3. Invoke the definition function with the resolved dependencies as arguments.
  4. Return the result of the definition function.

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 definition function will receive an array of resolved dependencies in the order they are declared in the deps array.
  • Dependency names within the definition function's deps parameter 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 Error objects 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 Map or object would be suitable.
  • The definition function's signature for dependencies might be implicit in this challenge (e.g., assuming deps[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 the deps array 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.
Loading editor...
javascript