一句简短的话把系统里的每一行代码逼到了墙角:"If you're not the Model, you're the Harness." 要么你是核心领域逻辑,要么你是为它服务的脚手架。没有第三种身份。
这个判断看似粗暴,实际上是一把裁纸刀——沿缝一划,架构里那些模棱两可的"既像业务又像基础设施"的代码立刻露出真面目。问题随之而来:谁主导谁?是代码去设计 Harness(先搭架子再填业务),还是 Model 驱动 Harnesses(先定领域再配基础设施)?
每个模块只有一张工牌
Model 是做出决策、承载规则、表达意图的部分。Harness 是让 Model 能跑起来的所有外围支撑:配置加载、日志埋点、数据库连接、HTTP 路由、测试夹具……它们不产生业务判断,只负责把 Model 的判断送到正确的地方。
模糊地带最危险。一段代码既读配置又做业务计算,既管事务又管折扣规则——它同时挂两张工牌,结果两张都挂不稳。重构时你不知道该把它归到 domain 包还是 infra 包,测试时你不知道该 mock 外部依赖还是构造纯领域输入。
Harness 先行:架子搭好,业务来填
很多项目从脚手架开始:选框架、建目录、配路由、写基类,然后"业务逻辑往里塞"。这种路径的标志是——你先决定了 Harness 的形状,Model 只能削足适履。
# Harness 先行:框架基类决定了 Model 的形状
class BaseController:
"""框架提供的基类,所有业务 Controller 必须继承"""
def __init__(self, db_session, logger, config):
self.db = db_session
self.logger = logger
self.config = config
def get_request_param(self, key: str):
# 框架从 HTTP request 中提取参数
...
def respond_json(self, data, status=200):
# 框架负责 JSON 序列化与 HTTP 响应
...
class OrderController(BaseController):
"""业务被迫继承框架基类,领域逻辑与基础设施交织"""
def apply_discount(self, order_id: str):
discount_rate = float(self.config.get("discount_rate", "0"))
order = self.db.query(Order).filter_by(id=order_id).first()
if order is None:
self.logger.warning(f"Order {order_id} not found")
return self.respond_json({"error": "not found"}, 404)
order.total = order.original_price * (1 - discount_rate)
self.db.commit()
self.logger.info(f"Discount applied: {order_id}")
return self.respond_json({"order_id": order_id, "total": order.total})
apply_discount 里,折扣规则、数据库查询、日志、HTTP 响应全部挤在一起。Model 的判断(折扣怎么算)被 Harness 的形状(基类注入了 db、logger、config)牢牢框住。换数据库、换传输协议、换日志库,都得改这段"业务代码"。
Model 驱动:领域先立,Harness 随形
反过来,先把 Model 写成纯领域对象——不依赖任何基础设施,只依赖它自己需要的概念。然后 Harness 围绕 Model 的接口来适配。
# ── Model:纯领域逻辑,零基础设施依赖 ──
from dataclasses import dataclass
from decimal import Decimal
@dataclass
class Order:
id: str
original_price: Decimal
def apply_discount(self, rate: Decimal) -> Decimal:
"""折扣规则完全在 Model 内部,不依赖任何外部服务"""
if rate < 0 or rate > 1:
raise ValueError(f"Invalid discount rate: {rate}")
self.total = self.original_price * (1 - rate)
return self.total
# ── Harness:适配层,把 Model 接到外部世界 ──
from flask import Flask, request, jsonify
app = Flask(__name__)
# Harness 负责加载配置、连接数据库、构造 Model 实例
DISCOUNT_RATE = Decimal("0.10") # 实际项目从 config/env 读取
def get_order_from_db(order_id: str) -> Order | None:
"""Harness 把数据库行映射为纯领域对象"""
# 假设使用 SQLAlchemy;此处省略 session 管理
row = db_session.query(OrderRow).filter_by(id=order_id).first()
if row is None:
return None
return Order(id=row.id, original_price=Decimal(row.original_price))
@app.post("/orders/<order_id>/discount")
def apply_discount_endpoint(order_id: str):
"""Harness 负责协议细节,Model 只管业务判断"""
order = get_order_from_db(order_id)
if order is None:
return jsonify({"error": "not found"}), 404
try:
total = order.apply_discount(DISCOUNT_RATE)
db_session.commit() # Harness 管事务
return jsonify({"order_id": order.id, "total": str(total)})
except ValueError as e:
return jsonify({"error": str(e)}), 400
if __name__ == "__main__":
app.run(debug=True)
关键变化:Order.apply_discount 只知道价格和折扣率,不知道数据库、HTTP、日志。Harness(Flask 路由 + DB 映射)围绕 Model 的公开方法来编排。换框架?重写 Harness,Model 一字不改。
用测试验证角色归属
最直接的检验方式是看测试能不能跑。Model 的测试不需要任何基础设施 mock——纯领域对象在内存里就能验证全部规则:
# Model 测试:零 mock,零外部依赖
from decimal import Decimal
def test_apply_discount_normal():
order = Order(id="A1", original_price=Decimal("100.00"))
result = order.apply_discount(Decimal("0.20"))
assert result == Decimal("80.00")
def test_apply_discount_rejects_invalid_rate():
order = Order(id="A1", original_price=Decimal("100.00"))
try:
order.apply_discount(Decimal("1.5"))
assert False, "Should have raised"
except ValueError:
pass # 预期异常
如果一段"业务测试"需要 mock 数据库、mock HTTP 请求、mock 配置中心——说明你的 Model 已经泄漏进了 Harness 的领地。该拆了。
采纳建议与自检清单
| 检查项 | Model 应该 | Harness 应该 |
|---|---|---|
| 依赖方向 | 只依赖其他 Model 或标准库 | 依赖 Model + 外部基础设施 |
| 测试方式 | 纯内存,无需 mock | 需要 mock/stub 外部服务 |
| 改动触发 | 业务规则变化 | 框架/协议/基础设施变化 |
| 包位置 | domain/ / core/ |
infra/ / adapters/ / handlers/ |
实操路线:
- 拿一个最核心的业务规则,把它从 Controller/Service 里抽成纯函数或纯类。 不带 db、不带 logger、不带 config 参数——只接收领域输入,返回领域输出。
- 为它写一个无 mock 的单元测试。 如果写不出来,说明抽取不彻底,回去再拆。
- 把原来混在一起的数据库/HTTP/日志代码推到 Harness 层。 Harness 调用 Model 的方法,Model 不调用 Harness 的任何东西。
- 新需求来时,先问:这是 Model 的变化还是 Harness 的变化? 答案决定你改哪个文件、加在哪个包下。
"If you're not the Model, you're the Harness" 不是一句口号,是一把尺子——量每一行代码,它到底在决策还是在服务。量清楚了,谁驱动谁就不再是争论:Model 定规则,Harness 做搬运,方向只有一个。