FDE Times 数据在招 FDE 职位 862▼55 近 7 天在招职位数的变化(同口径比较):不计入本周新开始追踪的公司的职位近 7 天新增 70在招公司 180支持远程 24%美国薪资中位数 $200k招聘最多 Palantir 95
ENVI

前线部署工程师(FDE)的专业媒体

书籍与课程

一本免费书,让你离场后集成代码依然好改

两位前 MADE.com 工程师合著,讲清一个 Flask 路由如何长成“大泥球”,以及怎样避免。

封面: Architecture Patterns with Python
Architecture Patterns with Python · Harry Percival · 封面图片:Open Library
分享微博

要点速览

  • 全书可在 cosmicpython.com 免费阅读,不要求读者事先了解 DDD
  • 值得记住的三个思路:Web 只是入口之一;Unit of Work 把操作归为一个整体;对外调用必须经过 outbox
  • 书中练习有难度但值得做,最好结合真实项目一起练
改造前后对比图。改造前:Flask 路由和夜间任务各自保留一份逻辑副本,两份副本逐渐不一致。改造后:路由、夜间任务和测试都调用同一个 sync_order 函数,逻辑只在一处。
按《Architecture Patterns with Python》服务层一章的做法,把逻辑从路由抽到用例中,夜间任务和测试无需复制代码即可调用。

一整本软件架构书就放在网上,免费可读。cosmicpython.com 以 Creative Commons 许可提供《Architecture Patterns with Python》全文,并附有 O’Reilly Learning 的链接。

作者是 Harry Percival 和 Bob Gregory。两人在 MADE.com 共事期间写下这本书。MADE.com 是一家在线销售家具的欧洲电商公司。

他们设想的读者,是那些被越写越乱的代码折磨过的 Python 开发者。前言说得很直白:不需要懂 DDD,也不需要了解经典架构模式。

对前线部署工程师(FDE)来说,这本书正好戳中这份工作最痛的地方。写出能跑的集成代码不难,难的是你离开项目之后,这些代码还能继续改。作者在前言里点明了这个问题:项目越大,最直观的写法越帮不了你管理复杂度。

泥潭从第一个接口开始

引言用一句很直接的观察引出问题:软件陷入混乱太常见了,工程师们甚至给它起了名字,叫“大泥球”(Big Ball of Mud)反模式。

设想一次典型的部署。你写了一个 Flask 路由:接收请求,读数据库,把数据推送到客户的 CRM,然后提交。演示跑通了。

# app.py — 改造前:所有逻辑都写在路由里
@app.route("/orders/<order_id>/sync", methods=["POST"])
def sync(order_id):
    order = db.session.get(Order, order_id)
    crm.push(order.to_dict())
    order.synced = True
    db.session.commit()
    return {"ok": True}

到了第三周,客户想要一个夜间任务,把所有数据重新同步一遍,于是你把中间那四行复制进一个脚本。现在逻辑有了两份副本,从第一次修改开始,它们就会各走各的路。

思路一:Web 只是入口之一

第 4 章针对的正是这种情况,给出的答案是服务层。按作者的说法,所有编排逻辑都放在用例层,业务逻辑则留在领域层。这里的编排指的是:从 repository 取数据、根据数据库状态校验输入、处理错误,一切正常时提交。

照这个思路改写示例,逻辑挪进一个独立函数,路由只剩几行:

# services.py — 一个用例,所有入口都调用它
def sync_order(order_id, session, crm):
    order = session.get(Order, order_id)
    crm.push(order.to_dict())
    order.synced = True
    session.commit()

# app.py — 路由只做 Web 层的事
@app.route("/orders/<order_id>/sync", methods=["POST"])
def sync(order_id):
    services.sync_order(order_id, db.session, crm_client)
    return {"ok": True}

夜间任务遍历订单,逐个调用 sync_order;测试则直接用假的 session 和假的 CRM 调用它。逻辑只在一处,接手代码的人也只需要读这一处。

到客户现场先做的一件事:在写第二个路由之前,把第一个路由的逻辑抽成用例。

思路二:Unit of Work 把操作归为一个整体

第 6 章引入 Unit of Work,这是书中对原子操作这一概念的封装:把必须同时成功或同时失败的操作归为一个整体。不显式调用 commit,就什么都不会保存。借助 Python 的上下文管理器,未提交的工作会在请求结束时自动回滚。

但再看一眼 sync_order:CRM 在提交之前就收到了数据。如果提交失败,CRM 里已经有了这笔订单,数据库却仍记录为未同步。回滚只能撤销数据库里的内容,撤不回已经发出去的调用。

把顺序反过来,先提交再调用 CRM,也救不了你。AWS Prescriptive Guidance 提醒:如果服务在提交事务后立刻崩溃,发给另一个系统的通知可能永远发不出去。

Confluent 把这称为双写问题:数据库更新成功,外部写入却失败,系统由此处于不一致状态。

思路三:对外调用必须经过 outbox

AWS 和 Confluent 给出的解法都是事务性 outbox。在保存订单的同一个事务里,把“要调用 CRM”这个意图写进 outbox 表,再由一个独立的 worker 负责调用、失败时重试、发送成功后标记完成。放进书中的 Unit of Work 后,sync_order 变成这样:

# services.py — 改造后:订单和 outbox 是一个整体
def sync_order(order_id, uow):
    with uow:
        order = uow.orders.get(order_id)
        order.sync_status = "pending"
        uow.outbox.add(event="order_sync", order_id=order_id,
                       idempotency_key=f"order-{order_id}")
        uow.commit()  # 要么都保存,要么都不保存

# worker.py — 独立运行,调用 CRM 并负责重试
def flush_outbox(uow, crm):
    with uow:
        for msg in uow.outbox.pending():
            order = uow.orders.get(msg.order_id)
            crm.push(order.to_dict(), idempotency_key=msg.idempotency_key)
            order.sync_status = "synced"
            msg.mark_done()
            uow.commit()

订单和 outbox 记录一起提交、一起回滚,因此不会再出现订单已保存、调用 CRM 的意图却丢失的情况。如果 CRM 出错,消息留在 outbox 里,等 worker 下次运行时重试。

代价是消息至少投递一次,也就是说 CRM 可能收到重复消息。AWS 建议让接收方具备幂等性,记录已处理过的消息,idempotency_key 承担的就是这个角色。如果客户的 API 不接受幂等键,worker 就得在重发前自己检查。

一个自测小练习:在 flush_outbox 中,假设 crm.push 成功了,但 worker 恰好在 uow.commit() 之前崩溃。此时订单和消息分别处于什么状态?下一次运行会做什么?先自己回答,再往下读。

答案:因为没有任何提交,Unit of Work 会回滚,消息仍是待处理,订单仍是未同步。下一次运行时,worker 用同一个 idempotency_key 重发,正是这个键阻止 CRM 创建第二笔订单。两半都答对了,你就明白为什么 outbox 和幂等性必须同时出现。

到客户现场先做的一件事:列出代码中所有既写数据库又调用外部系统的地方,逐一追问:第二步失败会怎样?

常见误区:还没痛就先加层

读完一本模式类的书,最大的诱惑是从第一个接口起就把每一层都搭出来。对 FDE 而言,每多一层,客户团队在你离开后就要多理解一样东西。

如果集成只涉及数据库,一个简单的提交函数依然够用。加层要有具体的痛点,比如逻辑被复制到了第二个任务里,或者某个对外调用可能丢失,而不是因为书里有这一章。

怎么读才不会被淹没

Giuliano Pertile 在 2022 年的一篇书评中仍然推荐这本书,但提醒书中给读者的练习相当难。这个提醒很中肯,也正是应该结合真实项目来读的原因。

比较合理的顺序:先读引言,理解代码为什么会滑向泥潭;再读服务层一章,然后是 Unit of Work。其余部分留到项目真正需要时再看。

从职业角度看,这是一项很容易展示的能力。一个个人仓库,路由足够薄,服务层清晰,带幂等键的 outbox,再加上一套不依赖真实数据库就能跑的测试,比简历上一句“整洁架构”更有说服力。FDE 面试中如果被问到你做过的集成,就讲讲外部系统中途失败时你是怎么处理的。

大多数客户永远不会读你的代码。但六个月后接手的工程师会读,而这本书正是为这个人写的。

这篇文章对你有帮助吗?

8 个信源
路线图上的下一篇 · 阶段 1: 基础认知修客户的 Python 代码前,先找出吞掉错误的 exceptBrett Slatkin 第三版共 125 条,更新到 Python 3.13,适合当作接手代码的检查清单,不必从头读到尾。