用 import-linter 为 Python 项目声明分层契约并拦截架构腐化
问题场景
Python 项目初期常把领域模型、用例服务和数据库/HTTP 适配器放在同一包中。随着迭代,domain 里可能出现 from myapp.infrastructure.repo import InMemoryOrderRepo 这类导入,领域层被迫感知存储细节,单元测试变慢,替换实现时牵一发动全身。代码评审也很难持续发现这种反向依赖。
import-linter 可以把分层约定写成契约,在本地和 CI 中做静态导入检查,让 domain 不依赖 application 或 infrastructure。
分层契约
概念上的三层是:
domain:实体、值对象、领域规则,不依赖外层。application:用例编排,可依赖domain。infrastructure:数据库、Web、消息等适配器,可依赖application和domain。
在 .importlinter 中,layers 从上到下表示“上层可依赖下层,下层不可依赖上层”。因此不能把 domain 写在最上面,而应按“外层在前、内层在后”排列:
[importlinter]
root_package = myapp
[importlinter:contract:layers]
name = Clean architecture layers
type = layers
layers =
myapp.infrastructure
myapp.application
myapp.domain
这样 infrastructure 可以导入 application 或 domain,application 可以导入 domain,而 domain 导入 application 或 infrastructure 会失败。如果团队只想先加一条最关键的规则,也可以改用 forbidden 契约,显式禁止 myapp.domain 导入外层模块。
最小可运行示例
目录结构:
myapp/
__init__.py
domain/
__init__.py
models.py
application/
__init__.py
service.py
infrastructure/
__init__.py
repo.py
.importlinter
myapp/domain/models.py:
from dataclasses import dataclass
@dataclass(frozen=True)
class Order:
order_id: str
amount: int
myapp/application/service.py:
from myapp.domain.models import Order
class OrderService:
def total(self, order: Order) -> int:
return order.amount
myapp/infrastructure/repo.py:
from myapp.domain.models import Order
class InMemoryOrderRepo:
def __init__(self) -> None:
self._orders: dict[str, Order] = {}
def save(self, order: Order) -> None:
self._orders[order.order_id] = order
上述代码符合契约:application 只依赖 domain,infrastructure 只依赖 domain。
运行方法
在项目根目录执行:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install import-linter
lint-imports
正常情况下会输出类似 Contracts: 1 kept, 0 broken. 的结果。为验证拦截效果,可临时在 myapp/domain/models.py 顶部加入:
from myapp.infrastructure.repo import InMemoryOrderRepo
再次运行 lint-imports,会看到契约被破坏,命令以非零状态退出;删除该导入后恢复通过。
在 CI 中拦截
把检查放进测试阶段即可,常见脚本步骤为:
- run: |
python -m pip install import-linter
lint-imports
lint-imports 发现反向依赖时返回非零退出码,CI 会直接失败,从而在合并前拦截架构腐化。建议在 CI 中固定依赖版本,避免工具自动升级导致规则行为变化。
边界与限制
import-linter做的是静态导入分析,动态导入、字符串拼接导入无法可靠识别。- 使用
src布局时,要确保myapp可被导入,例如安装为可编辑包或设置PYTHONPATH=src,否则契约无法正确解析模块。 - 相对导入、命名空间包、条件导入和类型检查块中的导入,建议以实际运行结果为准,必要时补充测试。
- 契约是开发期约束,不是运行时沙箱,不能替代单元测试和接口设计。
- 分层不宜过细;小项目可以先用一条
forbidden规则锁住domain,再逐步扩展。
- 点赞
- 收藏
- 关注作者
评论(0)