refactor: Make service dependencies explicit and injectable

Remove hidden cross-service coupling by making dependencies explicit through
dependency injection while maintaining backward compatibility via lazy imports.

Key changes:
- history_service and ban_service: Removed direct module-level imports of
  fail2ban_metadata_service, added optional service parameters to functions
- Added get_fail2ban_metadata_service() provider to dependencies.py
- Updated history router to inject Fail2BanMetadataService dependency
- history_service functions now use lazy imports in fallback paths for
  backward compatibility when service is not explicitly injected
- All test patches updated to use internal _get_fail2ban_db_path() helper
- jail_config_service and jail_service already follow best practices

This pattern prevents circular imports, makes services testable via explicit
mocking, and documents service dependencies clearly.

Fixes: Instructions.md #2 - Hidden cross-service coupling

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-27 18:26:08 +02:00
parent bc315b936b
commit 3bbf413c55
12 changed files with 342 additions and 100 deletions

View File

@@ -133,6 +133,47 @@ backend/
- **Repositories** handle raw database queries — nothing else.
- Never put business logic inside routers or repositories.
### Service Dependencies and Injection
Services should **never** directly import other services to avoid hidden coupling and make testing harder. Instead:
1. **Define clear service interfaces** using Protocol classes in `app/services/protocols.py`.
2. **Make dependencies explicit** by passing them as function parameters with optional defaults.
3. **Use lazy imports** for fallback singletons (not at module level).
4. **Inject services via FastAPI dependencies** when called from routers.
**Example:** The `history_service` depends on `Fail2BanMetadataService` to resolve the fail2ban database path:
```python
# Good — dependency passed as parameter
async def list_history(
socket_path: str,
fail2ban_metadata_service: Fail2BanMetadataService | None = None,
) -> HistoryListResponse:
if fail2ban_metadata_service is None:
# Lazy import fallback for backward compatibility
from app.services.fail2ban_metadata_service import default_fail2ban_metadata_service
fail2ban_metadata_service = default_fail2ban_metadata_service
...
```
Routers inject the service dependency explicitly:
```python
from app.dependencies import Fail2BanMetadataServiceDep
@router.get("/api/history")
async def get_history(
fail2ban_metadata_service: Fail2BanMetadataServiceDep,
) -> HistoryListResponse:
return await history_service.list_history(
socket_path,
fail2ban_metadata_service=fail2ban_metadata_service,
)
```
This pattern prevents circular imports, makes services testable, and allows easy mocking in tests.
---
## 4. FastAPI Conventions