Dependency Inversion Pattern
Overview
The Dependency Inversion pattern enables flexible component composition and lifecycle management by defining abstractions that high-level components depend on, while low-level components implement these abstractions. In NERV, this pattern drives the component wiring system with explicit dependency declarations and resolution.
Key Concepts
- Abstractions: Interfaces or protocols that components depend on
- Implementations: Concrete classes that fulfill abstractions
- Containers: Registries that manage component lifecycles and dependencies
- Providers: Factories that create component instances on demand
- Injectors: Mechanisms that supply dependencies to components
Benefits
- Decoupling: Components depend on abstractions, not implementations
- Testability: Dependencies can be easily substituted with test doubles
- Configurability: System composition can be modified without code changes
- Lifecycle Management: Component creation and disposal is centralized
- Explicit Dependencies: Dependencies are clearly defined and discoverable
Implementation Considerations
- Performance Overhead: Resolution of dependencies adds some runtime cost
- Complexity: Additional infrastructure for component management
- Type Safety: Ensuring correct dependencies at compile/runtime
- Circular Dependencies: Managing or preventing circular reference issues
- Scoping Rules: Determining component lifecycle and sharing policies
Implementation with Dependency Injector
The Dependency Inversion pattern in NERV is implemented using the Dependency Injector library, which provides a declarative and flexible dependency injection system for Python. It was chosen for the following reasons:
- Declarative API: Clean, declarative syntax for defining containers and providers
- Type Hinting Integration: Works well with Python type hints
- Performance: Efficient dependency resolution and caching
- Scoping Options: Support for different component lifecycles
- Runtime Configuration: Dynamic configuration capabilities
Core Dependency Injector Components
Component | Purpose |
---|---|
Container | Registry for component providers and configuration |
Provider | Factory for component creation with dependency injection |
Singleton | Provider that creates a single instance for the entire container |
Factory | Provider that creates a new instance each time |
Resource | Provider for resources requiring setup and cleanup |
Wiring Mechanism
Dependency Injector automatically wires dependencies using type annotations:
# Type definitions only - not full implementation
from dependency_injector.wiring import inject, Provide
@inject
def process_request(
provider_registry: ProviderRegistry = Provide[Container.provider_registry],
event_bus: EventBus = Provide[Container.event_bus]
):
"""Function with automatically injected dependencies."""
# Implementation using injected components
Pattern Variations
Hierarchical Container
Organizes dependencies in parent-child relationships for modular configuration.
# Type definitions only - not full implementation
from dependency_injector import containers, providers
class CoreContainer(containers.DeclarativeContainer):
"""Core service container."""
config = providers.Configuration()
event_bus = providers.Singleton(EventBus)
class AgentContainer(containers.DeclarativeContainer):
"""Agent component container with core dependencies."""
core = providers.DependenciesContainer()
agent_controller = providers.Singleton(
AgentController,
event_bus=core.event_bus
)
Factory Provider System
Creates component instances with contextual configuration.
# Type definitions only - not full implementation
class ConfigurableProviderFactory:
"""Factory for creating configured provider instances."""
@inject
def __init__(self, config: Configuration = Provide[Container.config]):
self.config = config
def create_provider(self, provider_type, model_name):
"""Create provider instance with specific configuration."""
provider_config = self.config.providers[provider_type]
return providers.Factory(
get_provider_class(provider_type),
model_name=model_name,
api_key=provider_config.api_key
)
Dynamic Registration
Allows runtime component registration for extensibility.
# Type definitions only - not full implementation
class DynamicContainer(containers.DeclarativeContainer):
"""Container supporting runtime registration."""
def register_component(self, name, component_class, dependencies=None):
"""Register a component at runtime."""
dependencies = dependencies or {}
# Create provider for component with dependencies
provider = providers.Singleton(component_class, **dependencies)
# Register provider in container
setattr(self, name, provider)
return provider
Component Lifecycle Management
The Dependency Inversion pattern carefully manages component lifecycles:
Singleton Components
Components with application-wide lifecycle, created only once.
# Type definitions only - not full implementation
class NERVContainer(containers.DeclarativeContainer):
"""Main NERV container."""
# Singleton components with application lifecycle
event_bus = providers.Singleton(EventBus)
temporal_store = providers.Singleton(TemporalStore)
state_projector = providers.Singleton(
StateProjector,
initial_state=providers.Dict(documents=providers.List())
)
Factory Components
Components created on demand for specific operations.
# Type definitions only - not full implementation
class ProviderContainer(containers.DeclarativeContainer):
"""Provider component container."""
# Factory components created per request
anthropic_provider = providers.Factory(
AnthropicProvider,
api_key=providers.Callable(get_api_key, "ANTHROPIC_API_KEY")
)
openai_provider = providers.Factory(
OpenAIProvider,
api_key=providers.Callable(get_api_key, "OPENAI_API_KEY")
)
Resource Components
Components requiring explicit resource management.
# Type definitions only - not full implementation
class ResourceContainer(containers.DeclarativeContainer):
"""Resource management container."""
# Database connection with proper lifecycle
database = providers.Resource(
Database,
connection_string=providers.Callable(get_connection_string),
# Resource cleanup after use
cleanup=lambda db: db.close()
)
Integration with Other NERV Patterns
Integration with Reactive Event Mesh
# Type definitions only - not full implementation
class EventDrivenContainer(containers.DeclarativeContainer):
"""Container for event-driven architecture."""
# Event bus as central component
event_bus = providers.Singleton(EventBus)
# Components depending on event bus
provider_manager = providers.Singleton(
ProviderManager,
event_bus=event_bus
)
agent_controller = providers.Singleton(
AgentController,
event_bus=event_bus
)
# Event listeners automatically wired
@containers.wire(modules=[".listeners"])
def wire_listeners(self):
"""Wire event listener functions."""
pass
Integration with Temporal Versioning
# Type definitions only - not full implementation
class VersionedContainer(containers.DeclarativeContainer):
"""Container with versioned components."""
# Temporal store for versioning
temporal_store = providers.Singleton(TemporalStore)
# Factory for versioned repositories
versioned_repository = providers.Factory(
VersionedRepository,
temporal_store=temporal_store
)
# Document repository with versioning
document_repository = providers.Singleton(
DocumentRepository,
versioned_repository=providers.Factory(
versioned_repository,
entity_type="document"
)
)
Integration with Effect System
# Type definitions only - not full implementation
class EffectDrivenContainer(containers.DeclarativeContainer):
"""Container for effect-driven components."""
# Effect handler registry
effect_handler = providers.Singleton(EffectHandler)
# Register effect handlers
@providers.init
def init_handlers(self):
"""Initialize and register effect handlers."""
self.effect_handler().register_handler(
EffectType.NETWORK,
providers.Factory(NetworkEffectHandler)
)
self.effect_handler().register_handler(
EffectType.DATABASE,
providers.Factory(DatabaseEffectHandler)
)
Performance Considerations
When implementing the Dependency Inversion pattern, consider these performance factors:
- Resolution Caching: Cache resolved dependencies for better performance
- Lazy Initialization: Create components only when needed
- Scoping Strategy: Choose appropriate lifecycle scopes for components
- Compile-time Wiring: Generate wiring code at build time when possible
- Injection Points: Minimize the number of injection points for critical paths
Related Patterns
- Inversion of Control
- Factory Method
- Abstract Factory
- Service Locator
- Strategy Pattern
Implementation Reference
See the implementation component: Container