跳到主要内容

第 12 章 服务端:业务逻辑的家

副题:分层、框架、中间件——请求到达服务器之后,业务规则住在哪里

第 11 章结束,请求终于到达了服务器:POST /v1/contents 的报文落在一台真实机器上。谁先接待它?怎么把报文变成业务逻辑的调用?业务规则(谁能发内容、内容多长、存到哪)又住在哪里?本章将会回答这三个问题——跟着一次请求,把整个后端走一遍。

本章要建立的认知

  1. 业务规则需要一个家:分层(表现/业务/数据访问)把"请求怎么处理"和"业务怎么决策"和"数据怎么存取"分开——每一层都有自己的职责;
  2. 框架是"请求 → 响应"的答案:路由(去哪)、中间件(路上办什么事)、控制器(接待);
  3. 分层有代价也有边界:过度分层会导致混乱,分层粒度也是决策,不是习惯。

12.1 一坨代码​

请求到达了。第一版后端处理"发布内容"的代码长这样:

app.post('/v1/contents', (req, res) => {
// 1. 检查登录
const token = req.headers.authorization;
if (!token || !verifyToken(token)) { res.status(401).json({ code: 'UNAUTHORIZED' }); return; }
const userId = getUserId(token);
// 2. 校验内容
const { title, body } = req.body;
if (!title || title.length > 100) { res.status(400).json({ code: 'INVALID_TITLE' }); return; }
if (!body || body.length > 10000) { res.status(400).json({ code: 'INVALID_BODY' }); return; }
// 3. 敏感词检查
if (containsBadWord(title + body)) { res.status(400).json({ code: 'BAD_WORD' }); return; }
// 4. 写入数据库
const id = db.insert('content', { userId, title, body });
// 5. 发通知
notifyFollowers(userId, id);
// 6. 返回
res.status(201).json({ id, title });
});

能跑。但一个月后,会出现很多问题。

问题一:改一个功能,崩了另一个。

产品说"发布内容要加个图片字段",你往第 2 步加校验。改完发现登录逻辑被影响了——第 1 步和第 2 步共用了某个变量,你没注意。没有边界的代码,改动像地震:震中在这头,裂痕在那头。

问题二:同一个逻辑,复制了三份。

"检查登录"在发布内容里有、评论里有、下单里也有,每处复制一份。后来登录规则改了(token 过期时间调整),你改了发布内容的,忘了改评论的——评论还能用旧 token 登录。复制三份的逻辑,迟早会变成三份不同的逻辑。 除了维护麻烦,还可能是安全漏洞(第 19 章会展开)。

问题三:没法测试。

想测"发布内容"这个业务规则,得先模拟 HTTP 请求、搭登录环境、连数据库。业务逻辑和 HTTP 报文、数据库绑死在一起,谁也测不了谁。

问题四:错误处理散落。

第 7 章说"错误响应结构必须全项目一致",但在一坨代码里,每个分支手写一行 res.status(400).json({ code: '...' })。有人写 BAD_WORD,有人写 badword,前端对应表根本对不上。错误码设计要落实,得靠代码结构来保证。

四个问题的根源是同一个:这段代码把四件不同的事混在了一起。

  • 请求怎么接待(HTTP)
  • 用户是谁(登录)
  • 业务规则是什么(能发什么内容)
  • 数据存在哪(数据库)

四件事的变化频率完全不同:接口格式偶尔变、登录规则几个月变一次、业务规则跟着产品变、数据库结构按版本变。变化频率不同的东西混在一起,改一个就要动全部。

这些问题和 9.1 的前端泥潭同一个原因:新手开发者会先跑通后组织——接口从两三个开始长,每个单独看都合理——"就几十行嘛"。一坨的演化通常分三个阶段:

  • 3 个接口时:完全没问题,一目了然
  • 6 个接口时:开始复制("检查登录"写第二遍了),但还能忍
  • 12 个接口时(第 7 章清单全部上线):复制三份的逻辑开始各改各的,改 A 崩 B 成为日常

没有谁一开始就把这样的代码写坏——它是每个功能加一行,慢慢长坏的。第 2 章的演进循环,在服务端又转完了一圈:接口数量上升 → 代码无法承受 → 分层出现 → 解决 → 新代价。

第 9 章的泥潭和本章的一坨,是同一个病的两种症状:前端泥潭是"状态同步"失控(改 A 忘 B),后端一坨是"职责混合"失控(改 A 崩 B)。病根都是"变化频率不同的东西混在一起"。第 9 章的解法是声明式(状态是唯一真相),本章的解法是分层(职责各归其位)。两个解法本质相同:把混在一起的东西分开。 下一节,把这一坨拆开。

12.2 分层:业务规则的家​

分层要做的,是把一坨代码按职责切成三层,每层只回答一个问题。

表现层(Controller) ← 请求怎么接待:解析参数、组装响应
↓
业务层(Service) ← 业务规则:谁能发、内容多长、发不发通知
↓
数据访问层(Repository/DAO) ← 数据怎么存取:读哪张表、写哪条记录
↓
数据库
图12-1 新 图稿占位
服务端分层
表现/业务/数据访问分层

三层各司其职

表现层是翻译官。它只做两件事:从 HTTP 报文里取出参数,把业务结果包成第 7 章约定的响应格式。内容合不合法、用户有没有权限——不归它管。

业务层是大脑。谁能发布内容、标题多长合法、敏感词检查、发不发通知——这些决策全住在这里。它不关心请求从哪来,也不关心数据存哪,只做判断和编排。

数据访问层是仓库管理员。业务层说"存这条内容",它负责写进 content 表;业务层说"查这个用户",它负责从 user 表读出来。数据库的细节全部封在这一层。


拆开后的代码长这样:

// 表现层:只负责接待和翻译
app.post('/v1/contents', (req, res) => {
const result = contentService.createContent(req.userId, req.body);
if (result.error) return res.status(result.status).json(result.error);
res.status(201).json(result.data);
});

// 业务层:只负责规则和编排
const contentService = {
createContent(userId, { title, body }) {
if (!title || title.length > 100) {
return { error: { code: 'INVALID_TITLE' } };
}
if (containsBadWord(title + body)) {
return { error: { code: 'BAD_WORD' } };
}
const id = contentRepo.insert(userId, title, body);
notifyFollowers(userId, id);
return { data: { id, title } };
},
};

// 数据访问层:只负责存取
const contentRepo = {
insert(userId, title, body) {
return db.insert('content', { userId, title, body });
},
};

拆完有什么好处?

第一,改动范围缩小了。 加图片字段只改表现层和业务层,登录逻辑完全不受影响——因为登录校验被抽到了中间件,不再和业务代码缠在一起。

第二,公共逻辑不用复制。 "检查登录"写一次,放在中间件里,所有接口共享。改 token 过期时间只改一处,不会出现"评论接口忘了改"的漏洞。

第三,业务层可以单独测试。 想测发布内容的规则?直接调 contentService.createContent(1, {title, body}),不用起 HTTP 服务,不用连数据库。传参数、看返回,这就是单元测试(第 18 章展开)。

第四,错误格式统一了。 表现层统一包装响应,业务层只返回错误对象,不会出现有人写 BAD_WORD、有人写 badword 的混乱。

分层和前几章是同一个思想。 第 6 章按业务对象分表,第 9 章按功能分组件,第 11 章把静态资源从源站拆到 CDN——本质都是"按变化频率分开,让改动只影响局部"。服务端代码也一样:接口格式偶尔变、登录规则几个月变一次、业务规则跟着产品变、数据库结构按版本变——混在一起,改一个就要动全部;分开后,各改各的。

两条纪律要守好。

上层调下层,不跨层。 表现层只能调业务层,不能直接查数据库;业务层只能调数据访问层,不能直接拼 HTTP 响应。一旦跨层,边界就成了摆设。

业务层按领域组织,不按接口组织。 叫 contentService(内容域:发布、列表、评论),而不是 publishContentService、listContentService。同一领域的规则住在一起,第 26 章拆服务时,这个边界就是服务的边界。

最后预告横向结构:中间件。

日志、鉴权这些"横切关注点"不属于任何一层,它们横着切过所有接口。请求进来先过中间件(解析、鉴权、日志),再进入三层纵向结构。服务端代码是"十字结构"——纵向是职责分层,横向是通用关卡,两条线交叉的地方就是每个具体接口。这个结构记好了,读任何后端代码都能快速定位"这段属于哪条线"。

12.3 框架:请求 → 响应的答案​

分层把职责拆清了,但拆好的层还需要一块地基才能跑起来:总得有人把网线上的字节流还原成 HTTP 报文,把报文交到表现层手上,再把响应写回网络。这些活每一项都是第 10 章教过的知识,每一项都与业务无关,而且每一个项目都要从头再来一遍——自己解析 TCP、自己拆报文、自己写路由匹配,是纯粹的重复劳动。把这类"每个项目都一样"的答案固化下来,就是后端框架:它内建了"请求 → 响应"的通用部分,你只需要回答"这个请求的业务逻辑是什么"。

框架长什么样,用 Express 看最直观(选型论证在本节末尾)。它最核心的能力是路由(Routing)——URL 和方法的组合,决定请求去哪。第 7 章接口清单里的 12 个接口,应用框架后就是对应的 12 条路由:

app.post('/v1/contents', ...); // 发布内容
app.get('/v1/contents', ...); // 内容列表
app.post('/v1/contents/:id/comments', ...); // 评论
app.post('/v1/orders', ...); // 下单

:id 是路径参数:框架自动把它从 URL 里取出来,挂到 req.params.id 上——第 7 章契约里的 {id},在代码里就是一行的事。版本化也在路由层实现:v1 是路由前缀,app.use('/v1', v1Router) 把整套 v1 路由挂在一起。

路由之外,框架还内建了一批每个项目都需要的能力:

  • 报文解析:req.method、req.headers、req.body,框架帮你拆好了;
  • 中间件机制:请求路上的关卡(日志、鉴权、解析 body、跨域 CORS);
  • 错误处理:异常统一转成第 7 章的错误响应格式;
  • 静态文件服务:图片、CSS 直接返回(上 CDN 之前先靠它)。

这份清单就是"框架帮你做了什么"的答案:凡是每个项目都一样的,框架做;只有你的业务才有的,你做。 不过在框架行为异常时(报文解析出错、路由匹配不达预期),懂协议的人能定位问题,不懂的人只能瞎试。协议知识是框架的调试手册。

框架也不是天生的,它同样是演进出来的:CGI 时代(1990 年代),每个请求启动一个进程跑脚本,慢且重;Servlet/ASP 时代,进程常驻了,但请求处理还是要手写;MVC 框架时代(2000 年代),路由、控制器、模板内建,"请求 → 响应"的答案成型;全栈框架时代(2010 年代),前后端一起管(Next.js/Nuxt)。每一代框架,都是上一代"手写太多"逼出来的——框架是重复劳动的固化,与炫技无关。

理解了这条线,"要不要用框架、用哪个"就只是一个普通的选型问题(再来回顾一下之前的后端选型):

业务目标:请求处理可维护,不重复造轮子。业务约束:阶段 1,单人开发,12 个接口,Node.js 已定(第 5 章)。技术问题:要不要用后端框架?用多重的?候选方案:A 手写(自己解析报文、写路由);B 轻框架(Express:薄而灵活);C 重型框架(Nest 一类:全套结构、约定多)。评价维度:生态(缺什么能不能装)、学习成本(单人团队学得动吗)、灵活度(能不能只装需要的)。方案选择:B(Express)——12 个接口的量级用不到 C 的全套结构,A 的重复劳动没有价值。收益:请求处理交给框架,业务只写规则。代价:框架的约定要学(中间件顺序、错误处理风格)。演进条件:接口数量与团队规模再上一个台阶时,评估更重的框架或自己封装。

12.4 请求处理链路:一个请求在服务端的完整走法​

分层和框架都有了,现在把一次请求在服务端的完整旅程走一遍——POST /v1/contents 从网线到数据库:

报文到达服务器
│
├─ 框架接住(Express:解析 HTTP,得到 req/res)
├─ ① 中间件1:请求日志 (记录:谁、什么时间、调了什么)
├─ ② 中间件2:解析 body (把 JSON 主体变成 req.body)
├─ ③ 中间件3:鉴权 (验证 token,把 userId 挂到 req 上——第 13 章主角)
│
├─ ④ 路由匹配:POST /v1/contents → 控制器
├─ ⑤ 控制器:翻译 (从 req 取参数,调业务层)
├─ ⑥ 业务层:决策 (校验/敏感词/存库编排)
├─ ⑦ 数据访问层:存取 (写 content 表)
│
└─ 响应原路返回:控制器组装 → 中间件(响应日志)→ 框架 → 报文
图12-2 新 图稿占位
中间件管道
请求穿过管道,每层横切关注点

这条链路里,中间件(middleware)是 12.2 分层之外的第二块拼图——12.2 说过它管"横切关注点",现在看它在链路上怎么布置:在请求到达控制器之前(和响应返回之后),插入一段代码,做日志、解析、鉴权、限流(第 24 章)这些事。如果每层都自己写日志,又是 12.1 的"复制三份"问题——中间件把横切关注点从各层里抽出来,放在请求的路上,一次写好,所有接口共享:

app.use(logMiddleware); // 所有请求先过日志
app.use(bodyParser()); // 所有请求先解析 body
app.use(authMiddleware); // 所有请求先过鉴权(第 13 章主角)
// 然后才是具体的路由
app.post('/v1/contents', controller);

中间件的顺序有讲究:日志要在最前(任何请求都要记录,包括失败的);鉴权要在路由前(没登录的请求不该到控制器);业务路由在最后(它们是"终点站")。顺序错了,行为就错——比如鉴权放在路由之后,没登录的请求也能进控制器,所以中间件的排列顺序,也是每次代码评审要看的第一行。

中间件还有一个"末端关卡":错误处理中间件(管道末尾统一接住异常:任何一层抛错,都流到这里,统一转成第 7 章的错误响应格式)和 404 中间件(所有路由都没匹配上时,统一返回 404)。它们的存在让"错误响应的结构必须全项目一致"(第 7 章)在代码层有了保证——12.1 那个'错误处理散落'的第四问题,答案是中间件。它必须放在所有路由之后:只有前面的关卡都过了、业务都跑完了,剩下的错误才轮到它。

控制器的"薄"与"厚"要立个规矩:控制器应该薄——只翻译(取参数、调业务、组响应),不决策(不写校验、不写业务判断)。"胖控制器"(把业务逻辑写在控制器里)是后端最常见的反模式之一:表面上分层了,实际是把 12.1 的一坨从函数搬进了控制器——把代码分进不同文件不算分层,把职责分清楚才算。

回看 12.1 的三个问题,现在都有了结构性的答案:检查登录从每个接口的复制三份,变成中间件里的一份(所有接口共享);改图片字段只动表现层的参数处理和业务层的校验,登录逻辑在中间件里,互不干扰;测试——中间件可以单独测、业务层可以单独测、控制器可以单独测。

把链路放回第 10 章的 TTFB:TTFB 慢,在服务端内部是这条链路的时间——中间件(日志/解析/鉴权)+ 控制器 + 业务层 + 数据访问层(数据库查询)。第 10 章说过,TTFB 慢是服务端侧的事(第 12/14 章的战场),现在有了内部地图:先看数据访问层(查库慢?第 14 章),再看业务层(决策慢?),最后看中间件(鉴权慢?第 13 章)——TTFB 的定位,从"服务端"细化到了"服务端的哪一层"。

层内还有一笔性能预算账(第 4 章验收标准的层内落点):第 7 章的 P95 预算(列表接口 < 200ms)摊到各层大概是——中间件 10ms、控制器 5ms、业务层 20ms、数据层 150ms(查询是大头,第 14 章主角)。预算不平均,但每层有数——性能优化时"哪层超预算"一眼可见(第 16 章展开)。

链路的异常路径也补一句:任何一层抛错,错误沿管道向后流动——中间件抛错(鉴权失败)→ 错误处理中间件接住转 401;业务层抛错(校验失败)→ 接住转 400/409;数据库抛错 → 接住转 500。错误处理中间件是链路的"安全网":正常路径上它是最后一道关卡,异常路径上它是唯一的接盘侠——12.1 的"错误处理散落",到这里彻底收拢成一道统一的后门。

把整条链路的代码拼起来看一眼(示意,每一段都是前面讲过的):

// 中间件:日志 → 解析 → 鉴权(一次写好,所有接口共享)
app.use(logMiddleware);
app.use(bodyParser());
app.use(authMiddleware); // 把 userId 挂到 req 上

// 路由 + 控制器(薄:只翻译)
app.post('/v1/contents', (req, res) => {
const result = contentService.createContent(
{ userId: req.userId, title: req.body.title, body: req.body.body } // DTO
);
if (result.error) return res.status(result.status).json(result.error);
res.status(201).json(result.data);
});

// 错误处理中间件:统一兜底(安全网)
app.use((err, req, res, next) => {
res.status(500).json({ code: 'INTERNAL_ERROR', message: '服务器开小差了' });
});

从中间件到控制器到错误兜底——第 12 章的所有概念,在这一小段代码里全部到齐。这就是"一个请求在服务端的完整走法"的代码形态:短,但每一行都有它的位置。

链路还是第 25 章可观测性的起点:每一层都打一条日志(中间件记请求进来、业务层记决策结果、数据层记查询耗时),配合第 7 章的 request-id 贯穿——"这个请求在每层花了多久"一目了然(第 25 章的链路追踪,就是这条日志链的自动化版)。分层不只是代码结构,还是排障地图和监控地图——同一个结构,三种用途。

链路的"异步"底色(Node 的运行时事实):Node 是事件驱动的——请求处理不占线程,数据库查询(第 14 章)、外部调用(第 10 章)都是异步的(await 就是"等它回来再继续")。这让 Node 能用单线程扛大量并发(查询在等 IO 时不阻塞其他请求),但也让"错误处理"更依赖中间件兜底(异步异常容易逃出 try-catch)。第 16 章性能优化时,"事件驱动为什么扛并发"是 Node 系优化的根本原因——这里先记住:链路的每一段都可能是异步的,await 是 Node 世界的"等一等"。

把第 12 章的请求链路和第 8 章的渲染管线放在一起看:两条管线是同一个形状——输入 → 逐段处理 → 输出(渲染:解析→树→布局→绘制;请求:中间件→控制器→业务→数据)。第 8 章说"管道思维"是本书反复出现的模式(找'卡在哪一段'),第 12 章是它在服务端的又一次实例——你在浏览器站学到的排查方法(分段定位),在服务端站原样可用。

12.5 DTO 落地:接口和表之间的缓冲​

DTO 是**"接口承诺的代码化"**:第 7 章契约里写了什么字段,DTO 就有什么属性。它站在接口这一侧,帮接口挡住表结构的变化。

ORM 才是**"表结构的代码化"**:第 6 章建了几张表,ORM 就生成几个模型类。它站在数据库这一侧。

DTO(Data Transfer Object)就是各层之间传数据的"包裹"。

请求进来,表现层从报文里取出参数,装进一个 DTO,往下传给业务层;业务层处理完,再装进另一个 DTO,返回给表现层组装响应。三层之间不直接传零散变量,而是传有固定形状的箱子。

// 表现层:把请求参数包成 DTO
const dto = { userId: req.userId, title: req.body.title, body: req.body.body };
const result = contentService.createContent(dto);

DTO 解决的核心问题是:隔开变化。

假设 content 表加了字段(比如 status),数据访问层知道就行,业务层和表现层用的 DTO 里没有这个字段,完全不受影响。反过来,接口要加返回字段,只改表现层的响应 DTO,业务层和数据层不动。每一层只认自己熟悉的形状,层与层通过 DTO 隔开,互不干扰。

这就是第 7 章"接口是承诺"的服务端实现。承诺的是 DTO 的形状,底层表结构怎么变,接口对外保持不变。

DTO 和契约是什么关系?

第 7 章的接口契约,在代码里的形态就是 DTO:

  • 请求 DTO(CreateContentDto)对应契约的参数定义
  • 响应 DTO(ContentResponse)对应契约的返回结构
  • 错误 DTO 对应第 7 章的 code/message/request_id 格式

契约写的每一个字段,都会变成 DTO 的一个属性。 DTO 命名清晰,接口文档都不用查;命名混乱(data1、obj),契约就跟着乱。

DTO 和 ORM 怎么分工?

第 6 章讲过 ORM 负责"数据库行 ↔ 程序对象"的翻译。现在看完整链路:

  • DTO:管接口侧的翻译(请求参数 ↔ 业务对象,业务结果 ↔ 响应格式)
  • ORM:管数据侧的翻译(程序对象 ↔ 数据库行)
  • 业务层:夹在中间,只做决策,不做翻译

六张表在代码里是六个模型类(ORM 管),十二个接口的出入参是十二个 DTO(DTO 管)。业务层只认 DTO,不认表。

代价是转换样板代码。

请求参数转 DTO、DTO 转数据库行、数据库行转响应对象——每层转换都有几行看似多余的代码。这是解耦的租金。不想付租金,就得忍受"表一变接口就变"的搬迁费。第 6 章说过"改表贵",DTO 就是让改表不那么贵的缓冲垫。

什么时候不需要 DTO?

两个信号:一是接口和表结构几乎一样,DTO 只是照抄,翻译样板多于缓冲价值;二是内部一次性脚本,没有对外承诺要守。判断标准还是第 4 章的复杂度预算:DTO 是"接口会变"的保险,接口不会变的地方,保险就是浪费。

案例的 12 个接口(第 7 章清单)全都配得上 DTO——因为它们是对外的承诺,承诺该买保险。

DTO 的完整生命周期像快递:

请求到达 → 表现层把参数装进请求 DTO → 业务层收下做决策 → 数据访问层把 DTO 翻译成数据库行 → 查完把行翻译回 DTO → 表现层把 DTO 组装成响应 DTO → 返回给客户端。

一次请求,DTO 在每一层都过一遍手。它不做事,但它是层与层之间唯一的通用语言。第 16 章性能优化时,"减少 DTO 拷贝"也是一类手段——大对象在层间反复拆包打包有开销。现在先认识它的旅程,优化留到后面。

12.6 分层的代价与边界​

分层解决了大问题,但也要说清它的代价和边界——分层不是越多越好。

**代价一:样板代码。**每一层多一个文件、多一层转换,小功能("改个状态")也要走完五层。**代价二:过度分层。**见过最极端的案例:一个"改昵称"的功能,拆了表现层/业务层/领域层/数据层/仓储层五层七个文件——改一行代码要翻五个文件。分层到这种程度就成了迷宫,谈不上组织。**代价三:新人要学约定。**一坨代码没有结构可学,分层的代码有一套"哪层放什么"的约定要学——团队里新人问"这个校验写哪层"是常态。这个代价没法省,只能靠两样东西缓解:命名清楚(文件名带层:contentController.js、contentService.js)和评审把关(第 20 章)——分层的约定是团队的共同语言,也是新人培训的第一课。

**边界:什么时候不需要严格分层?**三个信号——业务规则极少(接口基本是"查表返回",没有决策)、团队一人且项目临时(分层是给"会变"的代码准备的,一次性脚本不需要)、性能极度敏感(每层多一次调用,极致性能下是开销——但 99% 的项目不到这个程度,真到了那一步再说)。判断标准和第 4 章砍需求、第 9 章要不要框架是同一个逻辑:复杂度没到,就别分层。这也呼应第 4 章的复杂度预算:分层是复杂度预算的一种花法——先一坨跑通(预算省着花),业务规则真多了再分(预算花在刀刃上)。

到这里,第 5 章的九步模型已经应用了多次:语言选型(5)→ 数据库选型(5)→ 要不要 React(9)→ 要不要 CDN(11)→ DNS 托管(11)→ 框架选型(12)→ 分层粒度(12)——每一次都是同一个模型,换的是候选方案和评价维度,这就是"模型复用"的意义。

分层是团队协作的地图。团队里说"改这个接口",大家知道去表现层;说"改这个规则",知道去业务层;说"查这条数据",知道去数据访问层——分层让"改哪里"有了共同语言。在以后拆服务时,"哪些代码属于订单域、哪些属于用户域"的判断,靠的就是分层的边界;边界清晰的分层,拆起来是"搬文件",边界模糊的一坨,拆起来是"考古"。

安全审查也是按层进行——表现层看输入校验(参数有没有被构造过)、中间件看鉴权(谁能进来)、业务层看授权规则(谁能发、谁能删)、数据访问层看查询注入。分层的边界,就是安全审计的清单。限流也是中间件:限流中间件放在鉴权之后、路由之前——"这个 IP 每秒最多 10 个请求"。第 11 章说过 CDN 是"第一道墙",限流中间件是"服务端的第一道闸"——横切关注点清单(日志/鉴权/限流)会随着业务成长越来越长,中间件管道是它们的家。

**分层粒度是个决策,不是个习惯。**走一遍九步(轻量版):业务目标:业务规则可维护、可测试;业务约束:阶段 1 团队 1-2 人、接口 12 个、业务规则正在变多(内容/评论/交易);技术问题:服务端代码怎么组织?候选:A 一坨(12.1);B 三层(表现/业务/数据访问);C 五层(加领域层/仓储层);评价维度:可维护性、可测试性、样板成本;方案选择:B——三层够用,五层是为更大的团队和更复杂的领域准备的(第 26 章服务拆分时再评估);收益:业务规则有家、可测、可替换;代价:样板代码、多一层理解;演进条件:业务规则复杂度再上一个台阶(或团队变大)时,评估更细的分层,这就是第 9 步该有的样子。

把这一章放回第 2 章的演进链:一坨(阶段 1 起点)→ 分层(本章,被 12 个接口逼出)→ 服务拆分(第 26 章,被规模逼出)——每一次演进,都是"原方案无法承受"逼出来的(第 2 章的循环)。本章是这条链的中间一环:分层的边界,就是将来拆服务的边界。

12.7 本章对应表​

业务诉求技术选择为什么代价/取舍
业务规则要有个家分层(表现/业务/数据访问)独立职责、独立变化、独立测试样板代码、过度分层是另一种乱
请求怎么被接住框架(路由/中间件/控制器)内建"请求→响应"答案(第 9 章同思路)学习成本、框架的约束
横切逻辑(日志/鉴权)中间件管道一次写好、所有接口共享中间件顺序错了行为就错
接口和表解耦DTO(跨层传输)表变接口不变(第 7 章承诺的兑现)转换样板代码
分层粒度九步决策(三层就够)复杂度没到别过度分太粗耦合、太细迷宫

服务端按职责切三层:表现层接待请求,业务层管规则,数据层管存取。框架内建请求处理流水线,中间件统一处理日志鉴权等横切逻辑。DTO 在接口与数据库之间做缓冲,让表结构变化不影响对外契约。分层粒度走九步决策,阶段 1 三层够用,避免太粗耦合或太细迷宫。

本章小结​

  • 业务规则需要一个家:分层把"请求怎么处理"和"业务怎么决策"和"数据怎么存取"分开——每层都有自己的职责;
  • 框架是"请求 → 响应"的答案:路由(去哪)、中间件(横切关注点)、控制器(接待)——第 9 章"前后端框架同思路"的兑现,两个框架是夹着"你的业务"的两个翻译器;
  • 中间件把横切关注点(日志/鉴权)从各层抽出来放在请求路上——一次写好、所有接口共享;
  • 分层有代价也有边界:过度分层是另一种乱——分层粒度是决策,不是习惯;
  • 服务端站的第一章完成——下一站,业务的第一道门禁:认证与授权。

下一章​

服务端组织好了:分层清晰、链路通畅、请求能顺利到达业务层。但业务层开门之前,还差一道门禁:这个请求是谁发的?他有资格吗?