feat(phase2.4): add dependency injection container and factory wiring

Implemented Phase 2.4 (Dependency Injection Pattern):

- Added new dependencies module with DI container and runtime context
  - RuntimeContainer for injectable factories
  - RuntimeContext for resolved runtime objects
  - create_service_manager() factory
  - build_runtime_context() composition root
- Refactored main.py to use dependency container wiring
  - Main orchestration now resolves runtime via DI factories
  - Reduced direct constructor coupling in entrypoint
- Added unit tests for DI behavior with mocked dependencies
  - Verifies factory injection for service manager creation
  - Verifies runtime composition uses injected callables
- Updated optimization plan checkboxes for Phase 2.4

Validation:
- py_compile passed for new/changed files
- tests/unit/test_dependencies.py passed
- regression test test_event_queue::test_enqueue passed

Notes:
- Keeps existing runtime behavior unchanged
- Establishes clear composition root for future testability improvements
This commit is contained in:
Matthias Lotz 2026-02-18 23:17:22 +01:00
parent 0c19bd35d0
commit a4ea77e6b3
4 changed files with 138 additions and 23 deletions

View File

@ -603,7 +603,7 @@ iot_bridge/tests/
## 📦 Phase 2: Code Organization
**Status:** 🟡 Teilweise abgeschlossen (2.1-2.3 umgesetzt, 2.4 offen)
**Status:** 🟡 Teilweise abgeschlossen (2.1-2.4 umgesetzt, testlastige Restaufgaben offen)
**Aufwand:** ~6-8 Stunden
**Priorität:** 🟡 Mittel
**Abhängigkeiten:** Phase 0-1 abgeschlossen
@ -727,10 +727,10 @@ class MQTTConfig(BaseSettings):
**Dateien:** `dependencies.py` (NEU), `main.py`
**Aufgaben:**
- [ ] `dependencies.py` mit DI-Container erstellen
- [ ] Factory-Methoden für Service-Initialisierung
- [ ] Constructor-Injection statt globale Variablen
- [ ] Tests mit Mock-Dependencies schreiben
- [x] `dependencies.py` mit DI-Container erstellen
- [x] Factory-Methoden für Service-Initialisierung
- [x] Constructor-Injection statt globale Variablen
- [x] Tests mit Mock-Dependencies schreiben
**Erfolgskriterien:**
- ✅ Services werden als Dependencies übergeben

View File

@ -0,0 +1,56 @@
"""Dependency injection helpers for IoT Bridge runtime wiring."""
from dataclasses import dataclass
from typing import Callable
from core.bootstrap import BootstrapConfig, bootstrap, get_mqtt_config
from core.service_manager import ServiceManager
@dataclass(frozen=True)
class RuntimeContainer:
"""Container for runtime dependencies and factory callables."""
bootstrap_factory: Callable[[], BootstrapConfig] = bootstrap
mqtt_config_factory: Callable[[object], object] = get_mqtt_config
service_manager_factory: Callable[..., ServiceManager] = ServiceManager
@dataclass(frozen=True)
class RuntimeContext:
"""Resolved runtime objects used by main orchestration."""
boot_config: BootstrapConfig
service_manager: ServiceManager
mqtt_config: object
def create_service_manager(
boot_config: BootstrapConfig,
service_manager_factory: Callable[..., ServiceManager] = ServiceManager,
) -> ServiceManager:
"""Create service manager via injectable factory."""
return service_manager_factory(
config=boot_config.config,
logger=boot_config.logger,
bridge_port=boot_config.bridge_port,
bridge_token=boot_config.bridge_token,
)
def build_runtime_context(container: RuntimeContainer | None = None) -> RuntimeContext:
"""Resolve runtime context from dependency container."""
runtime_container = container or RuntimeContainer()
boot_config = runtime_container.bootstrap_factory()
service_manager = create_service_manager(
boot_config=boot_config,
service_manager_factory=runtime_container.service_manager_factory,
)
mqtt_config = runtime_container.mqtt_config_factory(boot_config.config)
return RuntimeContext(
boot_config=boot_config,
service_manager=service_manager,
mqtt_config=mqtt_config,
)

View File

@ -6,8 +6,7 @@ Clean entry point that delegates to bootstrap and service_manager modules.
This file should remain minimal (~80 lines) and focused on orchestration.
"""
from core.bootstrap import bootstrap, get_mqtt_config
from core.service_manager import ServiceManager
from dependencies import build_runtime_context
def main():
@ -20,27 +19,15 @@ def main():
3. Main Loop: Run until shutdown signal
4. Shutdown: Gracefully stop all services
"""
# Phase 1: Bootstrap
# - Parse command-line arguments
# - Load configuration (with persisted config fallback)
# - Setup structured logging
boot_config = bootstrap()
# Phase 2: Initialize Service Manager
# - Manages all service lifecycles
# - Handles graceful shutdown
service_manager = ServiceManager(
config=boot_config.config,
logger=boot_config.logger,
bridge_port=boot_config.bridge_port,
bridge_token=boot_config.bridge_token,
)
# Phase 1-2: Resolve runtime context via dependency container
runtime = build_runtime_context()
service_manager = runtime.service_manager
# Phase 3: Setup signal handlers for graceful shutdown
service_manager.setup_signal_handlers()
# Phase 4: Get MQTT configuration
mqtt_config = get_mqtt_config(boot_config.config)
mqtt_config = runtime.mqtt_config
# Phase 5: Start all services
# - Odoo client

View File

@ -0,0 +1,72 @@
"""Unit tests for dependency injection runtime wiring."""
from types import SimpleNamespace
from dependencies import RuntimeContainer, build_runtime_context, create_service_manager
def test_create_service_manager_uses_factory_with_boot_config_fields():
"""create_service_manager should pass boot config fields to injected factory."""
calls = {}
def fake_service_manager_factory(**kwargs):
calls.update(kwargs)
return "service-manager-instance"
boot_config = SimpleNamespace(
config={"some": "config"},
logger="logger-instance",
bridge_port=8080,
bridge_token="token-123",
)
result = create_service_manager(
boot_config=boot_config,
service_manager_factory=fake_service_manager_factory,
)
assert result == "service-manager-instance"
assert calls["config"] == {"some": "config"}
assert calls["logger"] == "logger-instance"
assert calls["bridge_port"] == 8080
assert calls["bridge_token"] == "token-123"
def test_build_runtime_context_uses_injected_container_factories():
"""Runtime context should be fully constructed from injected container factories."""
boot_config = SimpleNamespace(
config={"bridge": "config"},
logger="logger",
bridge_port=9000,
bridge_token="abc",
)
called = {"bootstrap": 0, "mqtt": 0, "service": 0}
def fake_bootstrap_factory():
called["bootstrap"] += 1
return boot_config
def fake_mqtt_factory(config):
called["mqtt"] += 1
assert config == {"bridge": "config"}
return {"broker": "test-broker", "port": 1883}
def fake_service_manager_factory(**kwargs):
called["service"] += 1
assert kwargs["bridge_port"] == 9000
assert kwargs["bridge_token"] == "abc"
return "service-manager"
container = RuntimeContainer(
bootstrap_factory=fake_bootstrap_factory,
mqtt_config_factory=fake_mqtt_factory,
service_manager_factory=fake_service_manager_factory,
)
runtime = build_runtime_context(container)
assert runtime.boot_config is boot_config
assert runtime.service_manager == "service-manager"
assert runtime.mqtt_config == {"broker": "test-broker", "port": 1883}
assert called == {"bootstrap": 1, "mqtt": 1, "service": 1}