Home
avatar

.𝙃𝙖𝙣

一个简单接口从请求进来到返回结果,中间到底过几层

这篇算是当前这个内部后台项目做到一半时,给自己记的一篇结构复盘。

项目背景很简单:主力开发当时更多在顶前台业务,我这边开始接一个内部后台,主要给运营用来控制素材、活动调配,还有一些基础配置。代码一写多,我很快就发现,后端麻烦的地方不在某一行语法上,而在这件事上:一个请求进来以后,中间到底该过几层,每层该干什么。

1. 先记一个最开始的错误理解

刚开始写接口的时候,我脑子里其实没有“层”这个概念。

当时最容易冒出来的写法就是:

  • 控制器收参数
  • 控制器里直接判断
  • 控制器里直接调数据库
  • 最后控制器把结果返回

这种写法在功能特别少的时候,好像也不是完全不能用。

但只要业务一多,就会马上出现几个问题:

  • 一个控制器方法越来越长
  • 参数校验、业务判断、数据查询全堆在一起
  • 同样的逻辑别的接口也要用,又得复制一次
  • 后面运营规则一改,不知道该动哪一层

所以这段时间我最想搞明白的一件事就是:

一个接口从请求进来到返回结果,到底哪些东西该放控制器,哪些东西该往下沉。

这个问题不想清楚,后面后台越做越大,只会越来越乱。

2. 先把我现在脑子里的最简流程记下来

我现在先不追求特别完整,先记一个当前够用的版本。

一个普通后台接口,现在我会先按下面这条线去理解:

  1. 请求进控制器
  2. 控制器接收参数 / 做基础校验
  3. 调用服务层
  4. 服务层处理业务规则
  5. 服务层操作实体 / 仓储 / 数据库
  6. 服务层组织结果
  7. 控制器返回统一响应

如果再套到我现在做的后台里,一个“新增素材”或者“修改活动配置”的接口,大概也都逃不开这条线。

我后来越来越在意的,其实是下面这两件事:

  • 每层有没有越界
  • 每层是不是只做自己该做的事

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. 这篇先给自己记一个当前可用版本

到目前为止,我对“一个简单接口从请求进来到返回结果,中间到底过几层”的理解,先记成下面这个版本:

  1. Controller 是入口,不是业务主战场
  2. DTO 负责接口的输入输出结构,不要和实体混用
  3. Service 负责业务规则和流程编排
  4. Entity 负责描述业务数据本身
  5. 数据层负责取和存,不要乱背业务逻辑
  6. 返回结果时再统一收口

这篇算是这个内部后台项目做到中期时的一个记录。

因为我现在已经明显感觉到,后端开发走到这一步,后面更绕人的往往是:

这个接口以后改需求、加字段、补规则的时候,会不会把自己写死。

先记到这里,后面回头再看现在这版理解稳不稳。

.NET WebAPI 后台系统 服务层 DTO