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:
parent
0c19bd35d0
commit
a4ea77e6b3
|
|
@ -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
|
||||
|
|
|
|||
56
iot_bridge/dependencies.py
Normal file
56
iot_bridge/dependencies.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
72
iot_bridge/tests/unit/test_dependencies.py
Normal file
72
iot_bridge/tests/unit/test_dependencies.py
Normal 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}
|
||||
Loading…
Reference in New Issue
Block a user