Hone logo
Hone
Problems

Implementing Dependency Injection in Python

Dependency injection (DI) is a powerful design pattern that promotes loose coupling between software components. This challenge will guide you through implementing a basic DI container in Python, allowing you to manage and inject dependencies into your classes, making your code more modular, testable, and maintainable. Understanding DI is crucial for building robust and scalable applications.

Problem Description

You are tasked with creating a simple dependency injection container in Python. This container should be able to register classes (or functions) as dependencies and resolve them when requested. The container should support resolving dependencies by name and injecting them into other classes.

What needs to be achieved:

  1. Registration: The container should allow you to register dependencies with a given name. The registered dependency can be a class or a function.
  2. Resolution: The container should be able to resolve a dependency by its name. If the dependency is a class, it should instantiate it. If it's a function, it should call it.
  3. Injection: The container should be able to inject dependencies into a class's constructor. The constructor should accept arguments corresponding to the dependencies, and the container should provide those arguments when instantiating the class.

Key Requirements:

  • The container should be able to handle both class and function dependencies.
  • The container should raise an error if a requested dependency is not registered.
  • The container should support injecting dependencies into classes with constructors that accept multiple dependencies.
  • The container should not modify the registered classes or functions.

Expected Behavior:

When resolving a dependency, the container should:

  • If the dependency is a class, instantiate it using the DI container to resolve its constructor arguments.
  • If the dependency is a function, call the function and return its result.
  • If the dependency is not registered, raise a DependencyNotFoundError exception.

Edge Cases to Consider:

  • Circular dependencies (A depends on B, and B depends on A). While a full circular dependency resolution is beyond the scope of this challenge, the container should not crash. It's acceptable to raise an error if a circular dependency is detected during instantiation.
  • Dependencies with no constructor arguments.
  • Dependencies that are functions returning other dependencies.

Examples

Example 1:

Input:
container = DependencyInjector()
container.register("logger", Logger)
container.register("database", Database)

class MyService:
    def __init__(self, logger, database):
        self.logger = logger
        self.database = database

    def do_something(self):
        self.logger.log("Doing something...")
        self.database.save("Data")

service = container.resolve(MyService)
service.do_something()
Output:
Logger: Logged "Doing something..."
Database: Saved "Data"
Explanation: The container registers Logger and Database, then instantiates MyService, injecting the registered logger and database instances into its constructor.  MyService then uses these injected dependencies.

Example 2:

Input:
container = DependencyInjector()
container.register("greeting", lambda: "Hello, world!")

message = container.resolve("greeting")
print(message)
Output:
Hello, world!
Explanation: The container registers a function that returns "Hello, world!".  Resolving "greeting" calls this function and returns the string.

Example 3: (Edge Case - No Constructor Arguments)

Input:
container = DependencyInjector()
container.register("simple_class", SimpleClass)

instance = container.resolve("simple_class")
print(instance.message)
Output:
Simple message
Explanation: SimpleClass has no constructor arguments. The container instantiates it directly.

Constraints

  • The solution must be implemented in Python 3.
  • The container should be relatively simple and easy to understand. Focus on the core concepts of DI.
  • The solution should handle at least one class dependency and one function dependency.
  • The solution should not use any external DI libraries.
  • The solution should be reasonably efficient for a small number of dependencies (less than 100). Performance is not the primary focus.

Notes

  • Consider using a dictionary to store the registered dependencies.
  • Think about how to handle the constructor arguments when resolving a class dependency.
  • You can define custom exceptions for error handling (e.g., DependencyNotFoundError).
  • This challenge focuses on the fundamental principles of DI. More advanced DI containers often include features like scopes, lifecycle management, and automatic dependency resolution. These are beyond the scope of this exercise.
  • The resolve method should return the resolved object (instance of the class or the result of the function).
  • Assume that all registered classes have valid constructors.
class DependencyNotFoundError(Exception):
    pass

class DependencyInjector:
    def __init__(self):
        self.dependencies = {}

    def register(self, name, dependency):
        self.dependencies[name] = dependency

    def resolve(self, dependency_name):
        if dependency_name not in self.dependencies:
            raise DependencyNotFoundError(f"Dependency '{dependency_name}' not found.")

        dependency = self.dependencies[dependency_name]

        if isinstance(dependency, type):  # It's a class
            return self._resolve_class(dependency)
        else:  # It's a function
            return dependency()

    def _resolve_class(self, cls):
        constructor_args = []
        try:
            constructor = cls.__init__
        except AttributeError:
            # Class has no constructor
            return cls()

        parameters = constructor.__annotations__

        for param_name, param_type in parameters.items():
            if param_name == 'return':
                continue  # Skip return annotation
            if param_name == 'self':
                continue # Skip self

            if param_name in self.dependencies:
                constructor_args.append(self.resolve(param_name))
            else:
                raise DependencyNotFoundError(f"Dependency '{param_name}' not found for class '{cls.__name__}'.")

        return cls(*constructor_args)


class Logger:
    def log(self, message):
        print(f"Logger: {message}")

class Database:
    def save(self, data):
        print(f"Database: Saved '{data}'")

class MyService:
    def __init__(self, logger, database):
        self.logger = logger
        self.database = database

    def do_something(self):
        self.logger.log("Doing something...")
        self.database.save("Data")

class SimpleClass:
    def __init__(self):
        self.message = "Simple message"
Loading editor...
python