我最近在复盘一个两年前做的系统瘦身项目原本一个查询接口从入口到数据库要经过六个类、三次接口转发、两次对象拷贝。每个中间层做的事情都一样把参数原封不动传下去把结果原封不动传回来。代码看着规整逻辑却完全多余。那次重构我们把这些中间层省掉接口平均耗时下降了将近四成核心代码量少了接近三分之一。也是从那时开始我对中间层省略这件事有了完整的判断框架。这篇文章不是概念科普也不是纯理论而是把我在实际项目里怎么识别冗余中间层、怎么安全地删掉它们、哪些层绝对不能动完整地讲一遍。如果你正在做系统治理、接口性能优化、代码瘦身或者只是被一堆层层嵌套的调用搞到头大这篇应该对你有用。新手朋友也不用担心我会把判断方法和操作步骤写得比较细照着盘一遍基本能上路。中间层省略不是否定分层架构而是去掉那些没有职责的转发层。一个工程系统该有多少层取决于业务复杂度和团队协作方式不取决于别人都这么写或加一层更保险的习惯。下面我按自己的实战经验把这个话题拆开讲。1. 中间层省略一次系统瘦身带来的认知升级1.1 一次真实的重构现场两年前某内部订单管理系统的核心查询接口订单详情查询出现了明显的性能问题。用户在前端点一下页面转圈快两秒客服反馈问题不断。拿到监控数据以后我先愣了一下数据库查询本身只花了大约40毫秒但接口整体耗时却有220毫秒。多出来的时间在哪里就在那一层一层的方法调用和对象转换上。我把调用链日志拉出来一条链路大致是这样走的Controller - OrderQueryService - OrderQueryManager - OrderQueryWrapper - OrderDetailAssembler - OrderDAO。OrderQueryService里有一堆业务规则判断这是合理的OrderQueryManager则基本什么都没干就是把参数透传给WrapperWrapper又调用了Assembler把数据库实体转成了展示模型。整个链路里Manager和Wrapper这两个类加起来快五百行却没有一行真正的业务逻辑。再仔细看Assembler的转换逻辑里有几个字段要从关联表二次查询可实际上这些字段在主线查询里已经查出来了等于重复查了一遍。这个场景可能很多团队都见过。代码不是一开始就长成这样的而是随着人员更迭和业务迭代慢慢堆出来的。最初可能只有一个Service和一个DAO后来有人觉得职责要清晰加了个Manager再后来有人觉得不能直接操作底层加了个Wrapper再后来为了统一返回结构加了个Assembler。每一层在加的当下都有自己的理由但经过几轮变化之后那个理由早就过期了层的转发还在。重构时我们做的事情其实很简单删掉Manager和Wrapper把业务逻辑收拢到ServiceAssembler只保留真正的字段映射删除二次查询原来需要经过四层调用的地方改成两层。两周时间涉及核心链路的改动文件从八个变成四个接口平均耗时从220毫秒降到130毫秒P99从580毫秒降到310毫秒。更重要的是后面再改这个接口的时候大家终于能在一屏以内看清完整逻辑了。1.2 中间层省略到底省略什么很多朋友听到省略中间层第一反应是分层架构不是最佳实践吗经典的三层架构、领域驱动设计里的分层以及微服务拆分不都是要分层吗这里必须先说清楚我们省略的不是分层而是中间层里的无效部分。一个健康的系统通常都有清晰的分层接入层负责协议解析和参数校验业务层负责业务规则和事务数据层负责持久化。这些层有各自明确的职责彼此之间是协作关系不是转发关系。这也是很多架构师的共识。问题出现在一种叫透传层的东西上。透传层的意思是说这一层没有自己的判断、没有自己的逻辑、没有自己的数据加工它就是把你传进来的东西原样递给下一层再把下一层返回的东西原样递回去。你问它负责什么它说我负责解耦你再问它到底解了什么耦它说不出来。所以中间层省略真正省略的目标是这几类东西没有逻辑的Manager/Wrapper/Helper、重复的对象转换层、形式主义的抽象接口、不解决实际问题的消息跳转。保留的是有真实业务逻辑的服务层、有安全职责的鉴权层、有隔离价值的外部系统防腐层、以及团队协作必需的契约接口。一句话总结省略的原则是层必须有事做没事做的层就是在给系统加熵。1.3 为什么工程师会堆出那么多中间层见过大量项目以后我总结了五个最常见的堆层原因几乎每个都能一一对上。首先是扩展性焦虑。担心业务未来会变复杂想着现在先留个抽象以后好扩展。但实际情况是业务的扩展方向往往和当初预想的不一样留出来的抽象根本用不上反而成了理解的障碍。其次是不敢删的心理。代码在线上跑着虽然没人说得清这层是干嘛的但谁也不敢保证删了不会出事于是所有人都选择不动中间层就变成了化石。第三个原因是架构规范被机械执行。团队规定接入层不能直接调数据层服务之间必须走接口本来是防止越层调用结果被理解成必须每一层都存在哪怕中间什么都没有。第四个原因是分工习惯。多个开发并行开发时为了避免冲突和相互等待有人会在中间再加一层好让彼此的代码可以不直接碰面。这种层本质上不是在解耦业务而是在解耦开发人员的心理压力。最后一个原因是对性能无感。本地开发时数据量小、调用快多几层感觉不到差别。到了线上高并发、大数据量场景每一层的方法调用、对象创建、内存分配都会变成可见成本。堆层还有个隐性代价层越多测试越难写。你想测一个业务逻辑得先构造三层以上的模拟对象最后测完发现你只是在验证一个透传方法有没有传对参数。这种测试写多了大家都不爱写测试质量自然往下掉。2. 值得省略的中间层四类典型盘查2.1 纯透传的门面层第一类最常见我给它起名叫门面层。典型命名是XxxManager、XxxWrapper、XxxHelper、XxxDelegate。你在IDE里点进去发现方法体只有一行return xxxService.doSomething(param);。这样的方法一个类里可能有好几十个整个类就是一张转发表。为什么会有这种类问十个人九个会说为了分层规范剩下一个会说别人都这么写。从业务角度看它没有增加任何价值从工程角度看它增加了一个编译单元、一个注入点、一个模拟目标还让阅读代码的人在类和类之间来回跳。判断方法很简单把这个类的方法全部列出来看每个方法有没有以下任何一种行为——修改入参、做条件判断、拼接数据、调用不同模块、抛出特定异常。如果都没有那这个类就属于候选删除对象。删的时候直接把调用方的引用从它替换成它内部调用的那个对象然后再把类删掉。需要注意一个例外情况如果这个门面层同时承载了框架的事务注解、权限注解、异步注解那它就不是纯透传了它是有隐性职责的。删除前要把这些注解的行为挪到真正该放的位置否则会踩坑。2.2 冗余DTO/VO转换层第二类是对象转换层。典型表现数据库实体Entity对应一个DTO对应一个VO然后每层之间还要做一次转换。代码写出来就是一行行的属性拷贝把entity拷到dto再把dto拷到vo。如果字段一模一样那这三个对象本质上是同一个东西的不同马甲。对象转换本身不是坏事它在外部协议和内部模型不一致的时候是必要的。但很多系统的转换层不是为了不一致而存在而是为了规范而存在。结果就是每次增删一个字段要同步改三个类、四处转化逻辑。改漏一个线上就报字段丢失团队天天在这种低级错误里消耗时间。省略的方法有两种。第一种是内部调用直接用领域模型只在对外接口边界做一次转换第二种是如果连对外模型都基本一致那就直接用同一个模型把转换层彻底拿掉。拿掉之后字段一致性由编译器保证新增字段只需要改一处。我见过最夸张的一个案例某数据导出功能一条数据在内存里被转了五次——Entity转DTO、DTO转BO、BO转VO、VO转ExportModel、ExportModel再转Map。每转一次都要遍历一遍字段列表数据量大的时候光转换就多跑了几秒。改成一进一出两次转换后时间直接减半。2.3 没有削峰需求的消息中间跳转第三类稍微隐蔽一点消息队列中间层。某个功能是同步查询为了体验异步化或者架构上必须有个队列把请求发到队列里再由另一个消费者去查库查完把结果写回缓存原请求轮询缓存拿结果。这套链路在流量低的时候看不出问题一旦请求量上来或者消费者消费不及时用户就要等很久还可能出现查不到结果的中间态。省略的原则在这里要拎清楚消息队列是削峰、解耦、异步化的工具不是装饰品。同步查询场景就不应该往队列里走直接同步调用即可只有在写多读少、削峰填谷、生产者消费者速率不匹配时队列层才值得保留。我遇到过一个支付通知查询系统原来是查库走直连的后来某次架构调整为了统一走消息通道把查询也塞进了队列。结果就是每次查询都要等消息被消费完才能拿到结果查询量一大队列积压接口超时率飙升。最终我们把查询链路从队列里放出来队列只保留真正需要异步处理的推送通知问题立刻缓解。当然如果你的业务确实需要削峰那队列层是必须保留的中间层不能因为追求省略就把队列也省了。省略的前提是这个中间层没有承担它该承担的职责而不是我讨厌所有中间层。2.4 形同虚设的历史抽象层第四类是历史抽象。比较典型的是一个接口一个实现类一个抽象工厂一个策略注册表实际上只有一种实现而且已经有两年没有增加过新实现。这种抽象留着除了让新同学多翻三层代码没有任何作用。抽象的价值在于应对多个变化维度。当你只有一个变化维度、一个实现类时接口和实现类的分离只是在制造间接层。这时候可以直接把接口删掉把实现类的方法下沉到使用方或者保留实现类作为普通类使用都是合理的简化方式。有一种情况要谨慎如果你的模块正处在重构期或者公司有多个技术栈、多套方案并行那么这个接口可能很快会出现第二个实现。这种情况下留着接口没问题但可以先把工厂和注册表省了等真有第二个实现时再加回来。工程上一个很重要的能力就是晚一点抽象而不是提前抽象。3. 省略中间层的实操方法从调用链分析到落地3.1 把调用链画出来用数据说话省略中间层听起来很爽但你不可能靠着感觉在代码库里乱删。我一般按四个步骤走第一步一定是画调用链。具体做法找一个线上接口或者一个核心用例打开日志平台输入请求流水号把整条链路的日志拉出来。如果你所在的技术体系有链路追踪工具那就更方便直接看调用链的节点列表每一层耗时、调用次数都清清楚楚。没有现成工具的话写个简单的切面打印每个方法的执行时间和参数跑一轮测试环境也有了。拿到数据后把每一层的耗时列成一张表。这是我常用来跟团队对齐的模板数据已脱敏调用层次方法/职责耗时占比是否有业务逻辑是否可省略接入层参数校验/协议转换2%是保留查询服务层业务规则、组装结果15%是保留管理器类透传调用0.4%否省略包装类透传调用0.3%否省略组装类实体转展示模型8%部分局部优化数据访问层数据库访问74%是保留这张表一出来结论是明摆着的。耗时大头在数据库访问中间层的耗时就那么零点几毫秒看似不起眼。但如果每次请求都多两次方法调用、多两个临时对象的创建在每秒几千次请求的规模下CPU和内存压力就真实反映在监控曲线里了。何况最核心的价值还不在于省那零点几毫秒而在于链路更短人能看懂改起来更快。3.2 给每一层做职责体检画完调用链下一步是给每一层做体检。我会用一个非常简单的检查清单逐层打勾这一层是否有独立的业务规则、条件判断、数据加工这一层是否承担了事务、权限、审计、异步等横切逻辑删除这一层后是否会导致其他模块无法独立演进或独立测试这一层是否在隔离外部系统比如第三方接口、旧系统、消息协议的变化这一层是否是团队之间明确的契约边界如果这几个问题全是否那它就是纯转发层直接进删除候选名单。如果有一个是就要再斟酌有些层表面上在做转发实际上承载了事务边界或者权限判断这种层不能简单删需要先把职责迁移到更合适的位置再让层本身消失。另外一个容易被忽略的点看这个类是否被很多地方引用。如果它只被一个调用方引用那么删除成本很低如果它被十多个调用方引用那即使它本身是透传也可能在扮演统一出口的角色。这种情况下除非你同时把那些调用方都改掉否则先别动它优先挑低风险的先做。3.3 小步重构每删一层单独提交体检完成之后真正动刀时我强烈建议小步走。哪怕是删一个看起来百分百安全的类也要遵守下面这个流程先补测试。给受影响的接口写关键路径的集成测试覆盖正常返回、异常分支、空数据三种情况。没有测试的话最次也要把线上日志的入参出参样本留一份。逐个删除不要一次删三个。每删掉一层编译、跑测试、部署到测试环境验证一遍。删除时先改调用方。把调用方里对中间层类的引用替换成它内部委托的那个实现类或方法确认逻辑不变后再删除类文件。每个步骤单独提交一次代码提交信息写清楚移除了X层Y调用方改直连Z。这样万一后面要回滚不会牵连其他改动。跑全量回归和接口自动化测试。重点看有没有之前依赖了中间层隐性逻辑的地方。上线时先灰度一部分流量观察日志和监控稳定后再放全量。这套流程看着慢其实很快。我改造上面那个订单查询接口前后也就两周大部分时间花在补测试和等回归上。真正的删除动作一个类只需要半天。3.4 量化收益让省略看得见删完之后别急着庆祝要把收益量化出来。数据是说服自己、说服团队、说服管理层的唯一硬通货。我通常做一份类似下面这样的对比表指标改造前改造后变化核心查询接口平均耗时220ms130ms-41%P99 耗时580ms310ms-47%核心链路涉及类数量8个4个-50%核心模块代码行数约3400行约2300行-32%修改一个字段平均改动文件数5个2个-60%这些数字拿出去比说一百句架构太冗余了都管用。而且注意一个点省略中间层带来的收益往往不只是性能。代码行数少了新同学上手时间缩短改动文件数少了提测范围变小、回归成本变低链路变短排查问题时的跳转次数也少了。这些软收益加分项也要写进总结里。4. 哪些中间层坚决不能省4.1 安全与权限边界省了可能出大事故省略中间层有个前提就是安全相关的层绝不能省。鉴权、越权校验、数据脱敏、审计日志、操作风控这些逻辑必须存在于独立的边界上不能被简化吞掉。举个例子某内部系统的管理后台原本在业务服务层做数据权限过滤检查当前用户能不能看某个部门的数据。某次性能优化时有人觉得这个检查每次都要查一次权限表太慢就把检查挪到了前端后端全靠接口地址来控制。结果没过多久就出事了只要构造一个未授权的请求参数就能直接拿到其他部门的数据。这就是省略错了位置。正确的做法是如果觉得权限检查放在业务层影响性能可以把它下沉到底层通用中间件用过滤器或者切面统一执行但绝不能省。安全边界是一条线线上的东西只允许移动位置不允许消失。4.2 外部系统的防腐层隔离价值无法替代第二个不能省的是防腐层也叫防腐层。当你的系统要对接第三方服务、旧系统、或者外部数据源时外部世界的数据格式、字段命名、异常方式跟内部差异很大。防腐层负责把外部模型翻译成内部模型把外部异常翻译成内部异常这样内部核心逻辑就不会被外部变化牵着走。很多团队的教训是觉得防腐层就是多包了一层直接让业务代码调用外部服务字段直接透传。刚开始确实省事但外部版本一升级字段改名内部所有用到这些字段的地方全部要跟着改外部系统出现一个特殊异常内部直接崩。防腐层省掉之后外部变化会瞬间传导到核心业务那个维护成本可比写一个转换类高得多。防腐层的省略原则是如果外部协议和内部模型已经强耦合且你无法控制外部变化节奏这个中间层坚决保留如果外部接口非常稳定、字段与内部一致并且外部系统属于公司内部可信基础设施那可以简化合并但至少要把异常转换边界留着。4.3 真正需要异步和削峰的链路别把缓冲带省了第三类不能省的是承担削峰、解耦、异步职责的队列层。这句话可能跟你前面看到的内容有点冲突前面我说同步查询不要走队列这里又说队列层不能省。其实两者不矛盾核心判断还是这个层有没有事做。如果你的系统面临突发流量比如秒杀、促销、批量对账生产者产生的请求速率远大于消费者能处理的速度队列就是那个缓冲带。把流量先削平消费者按自己的节奏处理系统才不会被瞬间冲垮。这种场景下省略队列层等于去掉了保险丝生产环境一上流量系统立刻出问题。同样当两个团队的模块之间需要解耦的时候消息队列作为中间层承载的是我不需要知道你现在是否在线、你也不需要等我处理完的异步契约。这种中间层不能省省了你就是把两个系统的生命周期绑死在了一起。4.4 团队协作的契约层该留还是得留最后一种不能省的情况不是技术层面的而是组织层面的。在一个大团队里模块A和模块B由不同的开发小组维护A调用B时B对外提供的接口就是一份契约。这份契约可以理解为中间层——它让A不用关心B内部怎么实现B也不用为了A随意改内部代码。这种契约层在紧急情况下看着像是多余的转发但它承担的是团队之间的协作边界。省掉它相当于让A直接依赖B的内部实现类B一调整内部结构A就得跟着动跨团队联调和沟通成本剧增。遇到这种情况我的建议是不要动它而是优化契约本身的质量让接口参数更少、含义更清晰。5. 常见问题与排查技巧实录5.1 删掉中间层后依赖突然碎了怎么办最常见的问题就是删掉一个看起来纯透传的层之后原本正常的流程突然出现空指针、状态值不对、或者异常信息变了。这时候不要慌先把代码恢复到删除前再去找原因。经验告诉我大多数看起来纯透传的方法里都藏着隐形逻辑可能在方法入口做了一个参数的非空判断可能在调用前把某个字段塞进了上下文可能默默地截获了一个异常再吞掉。排查技巧是在删除前用IDE的查找实现、调用层级功能把这个类的所有方法列出来逐个点进去看方法体里的每一行。凡是出现过条件判断、异常捕获、赋值操作、上下文写入、日志打印的行都要重点审视。把这些隐形逻辑迁移到调用方或者被调用方之后再考虑删类。简而言之先搬家后拆房。5.2 出现循环依赖和包结构混乱这种情况发生在同时删除多个层、或者大范围调整调用关系时。比如A依赖BB原来依赖C你把C合并进A后B又要依赖A循环依赖就出现了。解决办法是把公共的模型和少量公共方法拆到一个独立的包或者独立的小模块里让A和B都依赖它而不是互相依赖。另外我习惯在删除中间层之前就把包结构看一眼。如果发现某些类放在某个实现包下面但其实只是个中转站顺手把它调整到真正所属的模块里。包结构对了循环依赖会少很多。5.3 回归测试和灰度发布怎么做才稳妥很多朋友担心删了层线上出问题不好回退。我的建议是宁可慢一点也要让每次删除都隔离在一个版本里。如果一次提交同时改了多个接口、删了多个层出问题了你根本不知道该回滚哪一块。反过来每层单独删出问题直接定位到具体提交回滚也只影响那一个改动点。灰度方案上我会先把流量切一部分到一个新版本节点观察一段时间重点看错误日志、超时率、资源占用曲线。确认稳定后逐步放量。如果目标接口有自动化回归测试那就在测试环境先跑一遍没有的话让人工对着用例手动回归一遍核心功能。省略中间层这种重构最大的风险不是逻辑写错而是隐藏逻辑丢失所以验证重点放在行为是否等价上而不是代码是否更短。5.4 怎么跟团队解释简化的价值最后一个问题往往是最难的团队里有人反对觉得删代码是在破坏架构。我分享一下我的沟通方法。首先绝对不评价别人的代码是垃圾——那些中间层很可能是很多年前的合理决策只是今天不再需要了。其次用数据说话把调用链耗时、代码量、修改成本的前后对比甩出来。最后抓一个大家都能感知的痛点每次加字段要改几个文件、每次排查问题要跳几层。简化不是偷懒不写代码而是把精力留给真正需要思考的地方。我见过不少团队最终真正推动了中间层省略的往往不是架构师下指令而是某个一线开发在一个内部技术分享会上甩出一张让人血压升高的透传调用链截图。好的简化是会传染的。我自己在做过几次这样的改造之后最大的体会是省略中间层考验的不是编码能力而是对系统演进的理解和推倒重来的勇气。加一层很容易删一层很难难在你要说服自己、说服团队、说服那些看不见的万一。但凡是值得做的简化几乎都要经过这个过程。如果你正在做类似的事建议从最不起眼的一个查询接口开始删掉一个Manager跑一次全链路对比。当你真的看到那几毫秒、那几行代码消失的时候你会对中间层省略有一个完全不一样的感觉。
阅读完成 · 觉得有帮助?