一个简单接口从请求进来到返回结果,中间到底过几层
这篇算是当前这个内部后台项目做到一半时,给自己记的一篇结构复盘。
项目背景很简单:主力开发当时更多在顶前台业务,我这边开始接一个内部后台,主要给运营用来控制素材、活动调配,还有一些基础配置。代码一写多,我很快就发现,后端麻烦的地方不在某一行语法上,而在这件事上:一个请求进来以后,中间到底该过几层,每层该干什么。
1. 先记一个最开始的错误理解
刚开始写接口的时候,我脑子里其实没有“层”这个概念。
当时最容易冒出来的写法就是:
- 控制器收参数
- 控制器里直接判断
- 控制器里直接调数据库
- 最后控制器把结果返回
这种写法在功能特别少的时候,好像也不是完全不能用。
但只要业务一多,就会马上出现几个问题:
- 一个控制器方法越来越长
- 参数校验、业务判断、数据查询全堆在一起
- 同样的逻辑别的接口也要用,又得复制一次
- 后面运营规则一改,不知道该动哪一层
所以这段时间我最想搞明白的一件事就是:
一个接口从请求进来到返回结果,到底哪些东西该放控制器,哪些东西该往下沉。
这个问题不想清楚,后面后台越做越大,只会越来越乱。
2. 先把我现在脑子里的最简流程记下来
我现在先不追求特别完整,先记一个当前够用的版本。
一个普通后台接口,现在我会先按下面这条线去理解:
- 请求进控制器
- 控制器接收参数 / 做基础校验
- 调用服务层
- 服务层处理业务规则
- 服务层操作实体 / 仓储 / 数据库
- 服务层组织结果
- 控制器返回统一响应
如果再套到我现在做的后台里,一个“新增素材”或者“修改活动配置”的接口,大概也都逃不开这条线。
我后来越来越在意的,其实是下面这两件事:
- 每层有没有越界
- 每层是不是只做自己该做的事
3. 控制器:我现在把它理解成“入口层”,不要让它变成业务层
这个阶段我最容易犯的毛病,就是把控制器写胖。
因为控制器离请求最近,参数一进来,人会本能地在这里一直往下写。
但现在看下来,我觉得控制器最适合做的事情其实不多,主要是:
- 接请求
- 接参数
- 调服务
- 返回结果
它可以做一些很基础的判断,比如参数有没有传、格式对不对、模型绑定成没成功。
但如果开始在控制器里写这些东西,就要开始警惕了:
- 业务规则判断
- 多表联动逻辑
- 状态流转
- 复杂的数据组装
这些如果都塞进控制器,后面维护会很痛苦。
我现在给自己记一个很粗的判断法:
控制器尽量别回答“这件事该怎么做”,它只负责把请求送到该做事的地方。
这个提醒对我现在挺有用。
4. DTO:我现在把它理解成“接口这一层收和发的数据壳子”
前面刚开始看项目结构时,我对 DTO 这个词也有点虚。
现在先记一个最实用的理解:
DTO 是接口层拿来接参数、发结果的数据对象。
它和实体最容易混,但我现在会强行把它们拆开看。
DTO 更关心什么
- 前端会传什么
- 这个接口要收哪些字段
- 返回给前端的数据长什么样
实体更关心什么
- 数据库里这个业务对象本身长什么样
- 系统内部状态怎么表示
比如“新增素材”这个场景:
前端传进来的可能是:
- 素材名称
- 素材地址
- 素材类型
- 活动编号
这些可以先放在一个 CreateMaterialDto 里。
但数据库里的实体,除了这些字段之外,可能还会有:
- Id
- 创建时间
- 更新时间
- 删除标记
- 状态
这些就不一定都应该直接暴露给前端。
所以我现在会越来越倾向于:
DTO 和实体不要偷懒直接混用。
因为一旦混用,后面要么前端拿到一堆不该关心的字段,要么后端为了迁就接口,把实体本身也带歪。
5. 服务层:我现在把它理解成“接口真正开始做事的地方”
这一层对我现在最关键。
因为如果控制器只是入口,那“这件事到底怎么做”,总得有个地方收住。对我现在来说,这个地方就是服务层。
比如一个活动配置接口,服务层可能要做这些判断:
- 当前活动是否存在
- 当前状态是否允许修改
- 关联的素材是否有效
- 是否有重复配置
- 更新时间和操作记录怎么处理
这些判断如果不往服务层收,代码一定会散。
所以我现在看服务层,会优先想到它的两个责任:
第一,收业务规则
业务规则最好集中在这里,不要散在控制器和数据层里。
第二,编排流程
一次请求里如果要查数据、改状态、写日志、更新关联关系,这些动作怎么串起来,主要也应该在服务层控制。
我现在越来越觉得,服务层就是把一句“运营要改一个活动配置”,拆成后端能执行的一组动作。
6. 实体:还是那句话,先别让它背太多接口层的包袱
我现在对实体的理解,还是偏务实一点。
它首先应该表示业务数据本身,不要因为某个接口临时要用,就被拽成很奇怪的样子。
比如素材实体,核心更像是在描述:
- 这条素材记录是什么
- 它当前属于什么状态
- 它关联哪个活动
- 它什么时候创建和修改
它不应该因为“前端这个页面只想看两个字段”,就被迫改成另一个样子。
所以我现在对实体的一个提醒是:
实体优先服务业务本身,不要优先服务某个页面。
页面要什么,尽量通过 DTO 和结果组装去适配,而不是直接把实体拖过去裸奔。
7. 到数据库这一层,我现在先不想太复杂,但至少知道不能把业务判断全塞这里
目前我接这套后台,还没有把数据层想得特别花。
但至少有一点我现在越来越明确:
数据层更适合负责“取”和“存”,不适合承担太多业务判断。
比如:
- 按条件查询素材
- 根据 Id 找活动
- 保存更新结果
- 删除或修改状态
这些事情更像数据层该干的。
但像:
- 这个活动当前能不能改
- 这个素材能不能挂到这个活动下
- 这个状态流转合不合理
这些如果都写进数据层,后面边界会越来越糊。
所以我现在会更倾向于:
- 业务判断收在服务层
- 数据操作收在数据层
这样虽然不一定是最完整的架构,但至少脑子里不会全混成一锅。
8. 现在拿“素材/活动调配”这个后台场景,顺着走一遍
为了让自己别只停在概念上,我先拿当前这个后台的场景顺一遍。
假设现在有一个接口:
运营修改某个活动下的素材配置
我现在会把它大概拆成这样:
第 1 层:Controller
接收请求,比如:
- 活动 Id
- 素材 Id
- 排序
- 是否启用
然后把这些参数收进 DTO,再调服务。
第 2 层:DTO
负责把这次请求的输入结构定义清楚。
这样接口一进来,至少知道收的是哪几个字段,而不是到处散参数。
第 3 层:Service
这里开始真正做业务:
- 先查活动存不存在
- 再查素材存不存在
- 判断活动当前状态能不能修改
- 判断这个素材是不是已经挂过了
- 如果允许修改,就更新配置
- 如果需要,还顺手写操作日志
第 4 层:Entity / Data Access
这里处理数据落库。
比如:
- 读取活动实体
- 读取素材实体
- 更新中间配置关系
- 保存数据库
第 5 层:返回结果
服务层把结果整理好,控制器再统一返回给前端。
这样顺一遍以后,我自己最明显的感觉是:
每层拆开,最后都是为了别让所有问题堆到一个方法里。
9. 我现在对“一个请求要过几层”的结论很简单:边界比层数更重要
这个问题一开始很容易走偏。
因为一谈分层,就容易默认“层越多越正规”。
我现在反而更想提醒自己:
分层不是目的。更重要的是,请求在流转过程中,每一层都只承担自己该承担的事。
如果一个小接口本来很简单,硬拆七八层,最后代码也会发虚。
但如果一个中等复杂度的后台接口,什么都塞在控制器里,那后面也一定炸。
所以我现在对“几层”这个问题的阶段理解是:
- 不用追求形式上的多
- 重点是职责有没有收清楚
- 控制器、DTO、服务层、实体、数据操作,这几层至少先别混
对我现在这个阶段,这已经够用了。
10. 这篇先给自己记一个当前可用版本
到目前为止,我对“一个简单接口从请求进来到返回结果,中间到底过几层”的理解,先记成下面这个版本:
- Controller 是入口,不是业务主战场
- DTO 负责接口的输入输出结构,不要和实体混用
- Service 负责业务规则和流程编排
- Entity 负责描述业务数据本身
- 数据层负责取和存,不要乱背业务逻辑
- 返回结果时再统一收口
这篇算是这个内部后台项目做到中期时的一个记录。
因为我现在已经明显感觉到,后端开发走到这一步,后面更绕人的往往是:
这个接口以后改需求、加字段、补规则的时候,会不会把自己写死。
先记到这里,后面回头再看现在这版理解稳不稳。
