# 按职责组织服务端代码

原文: https://yishto.com/learn/service-data/service-layers-and-frameworks/

作者: [Yishto](https://yishto.com/about/)

按 HTTP 适配、用例、规则与仓储划分预约服务，验证事务、取消和依赖失败，并区分框架集成证据。

首次发布: 2026-11-10
内容更新: 2026-11-10

资料预约服务有输入解析、身份检查、名额规则、数据库与消息通知。如果所有动作写在一个 HTTP 回调里，规则很难脱离网络测试，失败时也难判断哪一步应该回滚。按职责组织代码，可以让业务规则有独立入口，再由服务用例协调外部操作。

本课以资料预约为架构案例，代码用合成价格列表演示规则与请求适配分开。它不启动 Express、Spring 或 Go 服务，不连接数据库；输出只证明纯函数与教学处理器的行为。

## 从用例确定责任边界

HTTP 适配负责解析输入、获得可信身份、限制请求体和映射响应；应用服务负责一个用例的步骤与事务边界；领域规则表达名额、价格或状态限制；仓储处理数据访问。边界决定依赖方向，不要求每个项目固定建四个文件夹。

预约用例可以接收用户 ID、资料 ID 与请求 ID，读取必要数据，验证名额，保存预留与结果。规则不需要知道请求头和数据库连接字符串；HTTP 层也不该把库存扣减写成散落在多个路由里的 SQL。

先列输入、输出、失败与副作用，再划分模块。只有一个简单规则时可以使用直接函数，没必要为了“企业架构”创建大量抽象工厂。复杂度增加后，把已经不同的责任拆开，比提前猜测所有未来场景更容易验证。

## 规则函数要明确接受什么

金额使用最小单位的整数，可以避免浮点表示带来的部分问题，但仍要定义币种、范围与四舍五入规则。数量也有上限，不应只判断非负。Python 中 bool 是 int 的子类，严格整数检查需要考虑是否允许布尔输入。

规则返回业务结果或明确错误，不直接构造 HTTP 响应。这样批处理、任务消费者和 Web 接口可以复用同一规则。失败时是拒绝、补偿还是重试，由用例与外部边界决定，不由纯计算函数自行发网络请求。

纯函数容易检查不变量，但不能用它证明存储并发或权限安全。预约名额仍需要数据库条件更新与约束。规则在应用层表达意图，数据库层保证多个写入者不能一起破坏核心条件。

## 应用服务协调事务与外部依赖

一个用例通常读取数据、检查规则、提交变化，再安排后续通知。将事务内必要读写集中处理，避免把长模型调用或文件上传留在锁持有期间。事务提交与外部消息发布不属于同一自动原子边界，需要 outbox、补偿或其他协调方案。

仓储接口按用例需要提供查询和操作，不必做一个万能 CRUD 抽象掩盖条件更新。保存预约时需要知道是否成功、是否重复和当前版本，而不是把任何结果都当成一个空对象。

调用外部依赖传递超时与取消条件。请求方离开时，如果底层工作可以取消，就及时停止；如果无法确定是否完成，应记录状态供恢复。取消一个界面等待不意味着数据库变化会自动撤销。

## 框架适配有不同约定

Express 用路由和中间件组合请求处理，顺序影响认证、解析和错误路径；具体错误处理方式应依据所用版本文档。Spring 常见 controller、service、repository 划分，但注解与目录名不会自动保证事务和业务正确。

Go net/http 使用 handler 处理请求，可以通过请求 context 传播取消与截止条件；连接和依赖调用仍需设置合适的超时。不同语言的并发模型不同，async I/O 不等于 CPU 并行，线程也不自动保护共享可变状态。

选择框架依据团队语言、依赖、部署和维护要求。一个模块化单体可以先把边界做清楚，不必一开始拆成微服务。拆服务会引入网络失败、版本与跨服务一致性问题，只有明确需求才值得承担。

## 用纯规则和教学适配器验证分工

total 拒绝负数与非严格整数，返回总额。handle 检查请求形状，将规则错误映射成 400，将成功映射成 200 与结果。示例中的金额 20、30 是合成最小单位，不代表实际价格。

```python
def total(items):
    if any(type(x) is not int or x < 0 for x in items):
        raise ValueError("invalid price")
    return sum(items)
def handle(payload):
    try:
        if not isinstance(payload, dict) or not isinstance(payload.get("prices"), list):
            return {"status": 400}
        return {"status": 200, "total": total(payload["prices"])}
    except ValueError:
        return {"status": 400}
assert handle({"prices": [20, 30]}) == {"status": 200, "total": 50}
assert handle({"prices": [-1]})["status"] == 400
print(handle({"prices": [20, 30]}))
```

输出为 `{'status': 200, 'total': 50}`。负数路径的断言检查请求被拒绝；规则独立于 HTTP 结构。这个模型没有完整价格上限、币种、身份或数据库，因此不是完整收费服务，也没有真正发送状态码。

如果把 total 直接改成访问全局请求对象，规则就重新依赖入口。可以对纯函数增加空列表、布尔值、字符串和大数测试，再对适配器检查输入形状与错误映射。真实 HTTP 层还需独立验证头、序列化与服务启动。

## 测试边界，不复制框架实现

规则检查覆盖不变量；用例检查依赖失败、事务与重复请求；HTTP 集成检查路由、身份、参数和响应。测试对象不同，不应让每个规则测试都启动完整服务器，也不应只测函数就宣布完整服务通过。

替身依赖需要表达失败行为，例如库存不足、保存冲突与通知不可达，不只永远返回成功。测试应断言外部效果次数、失败后状态与安全错误，而不是精确复制框架内部调用顺序。

共享状态放在模块全局时，必须考虑多请求并发与多实例。内存锁可能保护单进程，但不能代替数据库跨实例规则。日志记录用例与关联 ID，避免把所有内部异常以相同“失败”吞掉。

模块循环依赖通常提示责任没有理清。若规则反过来导入 HTTP 层，只为取得一个字段，应把该字段作为显式输入。共享辅助模块也不能变成任意业务都能写入的全局状态。

## 练习：完成一个预约用例

写出预约输入、允许状态、名额约束和重复请求语义。分别定义纯规则、仓储操作、应用服务和 HTTP 适配，明确依赖方向。用合成依赖模拟无名额、重复提交、保存失败与通知失败。

验收要求：规则可脱离框架验证；无效输入不调用写操作；成功预留只扣一次；保存失败不发成功通知；通知失败有可恢复状态；超时条件传到可取消依赖。实际框架集成应另外记录运行结果，代码层次图与纯函数断言不能证明完整部署已可用。

## 资料来源与延伸阅读

- [Express routing](https://expressjs.com/en/guide/routing.html)
- [Building a RESTful web service](https://spring.io/guides/gs/rest-service)
- [Go HTTP package](https://pkg.go.dev/net/http)

资料核验：2026-11-10。教学示例不代表线上实测。

## 相关文章

- [用测试和决策记录支持代码演进](https://yishto.com/learn/data-practice/testing-refactoring-and-decisions/)
- [项目：商品描述审阅与协作工作台](https://yishto.com/learn/project-labs/lab-product-description-review/)
