refactor(arch): eliminate all cross-module model imports in service layer
Some checks failed
CI / ruff (push) Successful in 9s
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / pytest (push) Has been cancelled

Enforce MOD-025/MOD-026 rules: zero top-level cross-module model imports
remain in any service file. All 66 files migrated using deferred import
patterns (method-body, _get_model() helpers, instance-cached self._Model)
and new cross-module service methods in tenancy. Documentation updated
with Pattern 6 (deferred imports), migration plan marked complete, and
violations status reflects 84→0 service-layer violations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 06:13:15 +01:00
parent e3a52f6536
commit 86e85a98b8
66 changed files with 2242 additions and 1295 deletions

View File

@@ -237,19 +237,24 @@ marketplace_module = ModuleDefinition(
### 4. Type Checking Only Imports
Use `TYPE_CHECKING` for type hints without runtime dependency:
Use `TYPE_CHECKING` for type hints without runtime dependency. Pair with `from __future__ import annotations` to avoid quoting type hints:
```python
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from app.modules.marketplace.models import MarketplaceImportJob
def process_import(job: "MarketplaceImportJob") -> None:
# With `from __future__ import annotations`, no string quoting needed
def process_import(job: MarketplaceImportJob | None = None) -> None:
# Works at runtime even if marketplace is disabled
...
```
> **Note:** Without `from __future__ import annotations`, you must quote the type hint as `"MarketplaceImportJob"` to prevent a `NameError` at class/function definition time.
### 5. Registry-Based Discovery
Discover modules through the registry, not imports:
@@ -265,6 +270,97 @@ def get_module_if_enabled(db, platform_id, module_code):
return None
```
### 6. Method-Body Deferred Imports
When a service needs to **query** another module's models directly (e.g., the model's owning service doesn't exist yet, or the service IS the gateway for a misplaced model), defer the import into the method body. This moves the import from module-load time to first-call time, breaking circular import chains and keeping the module boundary clean.
Choose the sub-pattern based on how many methods use the model:
#### 6a. Simple method-body import (1-2 methods)
When only one or two methods need the model, import directly inside the method:
```python
# app/modules/catalog/services/product_service.py
class ProductService:
def create_product(self, db, data):
# Deferred: only this method needs MarketplaceProduct
from app.modules.marketplace.models import MarketplaceProduct
mp = db.query(MarketplaceProduct).filter(
MarketplaceProduct.id == data.marketplace_product_id
).first()
...
```
#### 6b. Module-level `_get_model()` helper (3+ methods, same model)
When many methods in one file use the same cross-module model, create a module-level deferred helper to avoid repeating the import:
```python
# app/modules/monitoring/services/log_service.py
def _get_application_log_model():
"""Deferred import for ApplicationLog (lives in tenancy, consumed by monitoring)."""
from app.modules.tenancy.models import ApplicationLog
return ApplicationLog
class LogService:
def get_database_logs(self, db, filters):
ApplicationLog = _get_application_log_model()
return db.query(ApplicationLog).filter(...).all()
def get_log_statistics(self, db, days=7):
ApplicationLog = _get_application_log_model()
return db.query(func.count(ApplicationLog.id)).scalar()
def cleanup_old_logs(self, db, retention_days):
ApplicationLog = _get_application_log_model()
db.query(ApplicationLog).filter(...).delete()
```
#### 6c. Instance-cached models (pervasive usage across class)
When a model is used in nearly every method of a class, cache it on the instance at init time to avoid repetitive local assignments:
```python
# app/modules/marketplace/services/letzshop/order_service.py
def _get_order_models():
"""Deferred import for Order/OrderItem models."""
from app.modules.orders.models import Order, OrderItem
return Order, OrderItem
class LetzshopOrderService:
def __init__(self, db):
self.db = db
self._Order, self._OrderItem = _get_order_models()
def get_order(self, store_id, order_id):
return self.db.query(self._Order).filter(
self._Order.id == order_id,
self._Order.store_id == store_id,
).first()
def get_order_items(self, order):
return self.db.query(self._OrderItem).filter(
self._OrderItem.order_id == order.id
).all()
```
#### When to use each sub-pattern
| Model usage in file | Pattern | Example |
|---------------------|---------|---------|
| 1-2 methods | 6a: method-body import | `product_media_service.py``MediaFile` |
| 3+ methods, same model | 6b: `_get_model()` helper | `log_service.py``ApplicationLog` |
| Nearly every method | 6c: instance-cached | `letzshop/order_service.py``Order` |
> **When NOT to use these patterns:** If the owning module already has a service method that returns the data you need, call that service instead of querying the model. Deferred imports are for cases where the model's service doesn't expose the needed query, or the service IS the canonical gateway for a misplaced infrastructure model.
## Architecture Validation
The architecture validator (`scripts/validate/validate_architecture.py`) includes rules to detect violations: