refactor(arch): eliminate all cross-module model imports in service layer
Some checks failed
Some checks failed
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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user