Hone logo
Hone
Problems

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:

  1. 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.
  2. 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.
  3. Handle Missing Dependencies: If a requested dependency is not registered, the container should throw an error.
  4. 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 Container class should have a register method to register dependencies.
  • The Container class should have a resolve method 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) registers MyLogger as a dependency with the key 'logger'.
  • register('api', () => new ApiClient()) registers a new ApiClient instance each time resolve('api') is called.
  • resolve('logger') returns the registered instance of MyLogger.
  • resolve('api') returns a new instance of ApiClient.
  • 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 Container class 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.length to 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.
Loading editor...
javascript