
一次评审会上年轻同事指着一份设计文档问我“这个Manager层是不是有点多余了我数了一下一个查询从Controller进来要经过Service、Manager、Handler最后才到Mapper每一层代码长得几乎一样。”会议室安静了几秒然后有人接话“架构嘛分层清晰一点以后好扩展。”我特别理解这种反应。过去十多年我见过太多类似的代码库——每一层都像俄罗斯套娃一样拆开一个里面还是一个一样的娃娃。但“架构”这两个字不该等同于“层次多”。架构的职责是控制复杂不是制造复杂。今天这篇东西我就想聊聊为什么简单的代码胜过无脑的层次以及我自己在项目里用来判断“该不该加这一层”的几条实用标准。1. 先把话说清楚架构的职责是控制复杂不是制造复杂1.1 为什么我们默认“层多就等于专业”先说个现象很多程序员尤其是刚工作三五年、正好开始参与系统设计的阶段都有一种“层数越多越正规”的错觉。这种错觉不是凭空来的它受几个因素影响。首先是历史遗留的教育路径。很多经典教材讲企业级开发一上来就是表现层、业务层、数据访问层层层分明。教材为了把职责讲清楚自然要划分边界但读者容易把“教学示例”当成“生产标准”。其次是公司内部的历史代码本身就是这么写的——老项目的三层结构被当成“祖传规范”新人来了照着加不问为什么。最后是面试文化推波助澜八股文里常问“你项目的架构是什么样的”候选人也倾向于把分层讲得越细越好似乎层数多就能显得有设计感。结果就是在很多代码库里分层不再是控制复杂度的工具反而成了复杂度的来源。我见过一个中等规模的订单系统一个查询用户订单的接口从Controller进去先过Service再过OrderManager再过OrderHandler然后才到OrderMapper查询数据库。任何一个中间层都只是转发一下各层之间甚至没有不同的逻辑职责。这种“套娃”架构本质上就是把一个本来两步能走完的路硬拆成五个检查站。这里要澄清一个概念架构的价值不在于“有多少层”而在于“每一层是否承担了独特且必要的职责”。如果两个连续层做的事情没有本质区别那它们就不该作为两个独立层存在。1.2 间接层次的真实成本很多人以为多包一层只是多点代码量无伤大雅。实际上间接层次有很具体的成本而且这些成本会在项目演进过程中被放大。第一是心智负担。人脑的工作记忆是有限的一个调用链从起点到终点每多一个间接层阅读代码的人就要多记一个跳转。十层的调用链和五层的调用链阅读难度不是线性增长是指数增长。因为每跳一层你都要在脑子里保存“上一层的意图”和“当前层的职责”两个上下文。第二是调试和排查问题的成本。线上出了Bug一个查询结果不对你得沿着调用链一层一层往下跟。每层都可能有一点点数据处理到底哪一层把字段改错了断点打在哪一层如果所有层都是直接转发排查成本和调用链长度成正比而收益为零。第三是性能损耗。这个得分情况说一般业务系统里多几层Java方法调用对性能影响微乎其微几乎可以忽略。但如果是高频路径或是有代理、有反射、有动态增强的层次每多一层都是实打实的开销。更重要的是一旦你为了“可扩展性”先铺了很多层后来要在层与层之间塞逻辑比如加个缓存、加个审计你会在错误的层之间反复试探改错地方的情况非常多。第四是改动的连带成本。一个简单需求的改动本应只动一个地方套娃架构下往往要动三处以上接口加一个参数所有实现类要改中间转发层也要改。这不是夸张我见过给一个查询加个“是否包含已删除订单”的布尔参数结果改了九个文件的案例。所以不要轻易引入一个层。每一个间接层都要问一句它到底屏蔽了什么变化还是仅仅在传递调用2. 无脑层次的三种典型症状2.1 为“可扩展性”预支的结构无脑层次最常见的理由是“万一以后要扩展呢”。我特别理解这种未雨绸缪的愿望但它经常演变成“为不存在的变化预先设计结构”。举个例子。某系统现在只有一种订单来源——小程序下单。有人设计了一个OrderSource接口下面只有一个WechatOrderSource实现类然后所有业务代码都通过接口调用。理由是“以后肯定会接入App、接入第三方平台到时候只要加实现类就行业务代码不用改。”这就是典型的预支抽象。问题在于你现在根本不知道未来接入的新来源会带来什么差异。也许新的来源根本没有“优惠券抵扣”这个概念也许新的来源有“门店自提”流程这些差异会深刻影响业务代码的结构。你现在抽象的OrderSource接口大概率在真正的第二来源到来时被推翻重来因为变革不在你预想的方向上。YAGNI原则You Aren‘t Gonna Need It你不会需要它很多时候被误读为“不要写多余代码”其实它更深层的含义是不要在信息不足的情况下做结构性决策。等真正的变化来临时你掌握的信息足够多做出的抽象往往比现在的空想准确得多而且重构成本未必高——因为当时的代码简单改动范围清晰。我现在的基本立场是只有一份实现时不建接口。等待第二个实现出现时再抽取接口。这不是懒而是让抽象来自真实需求而不是想象。2.2 只为“对称美”设计的同构层另一种常见的套娃是纯粹为了形式上的对称。团队定了“Controller-Service-Mapper”三层结构于是所有功能都套这个模板不管这个功能本身多么简单。比如一个“根据ID查询用户姓名”的接口。Controller调Service的getUserNameByIdService里没有任何业务逻辑直接调UserMapper的getUserNameByIdMapper查数据库返回。这三层代码从方法名到参数到返回值几乎完全一样。为什么要分成三层因为“规范”要求这么分层。这就是对称美驱动的设计——每一层都必须存在否则看起来“不完整”。这种同构层的危害除了前面说的心智负担之外还有一个不易察觉的点它会掩盖真正的职责边界。当一个Service方法只做转发时程序员会把一些本不该属于Service的逻辑也塞进Service因为“反正这里有个方法往里面加点东西方便”。于是Service越来越臃肿而Controller和Mapper层又太薄职责边界变得混淆不清。更好的做法是让每一层的存在都由职责驱动。没有业务规则需要统一收口时Controller直接调Mapper也不是不行。等到真的出现“多个Controller都要用同一个业务规则”时再把这条规则提升到Service层。层次应该是“长”出来的而不是“套”上去的。2.3 被“禁词化”的设计原则还有一类套娃是设计原则被背成了教条。单一职责原则变成了“一个类只干一件事”于是有人把原本一个类里自然的几个方法拆成五个类每个类一个方法开闭原则变成了“对扩展开放”于是做任何修改都想着加一层而不敢动已有的类。我曾经接过一个老系统的维护里面有个订单状态的流转逻辑本来在一个状态机类里还算清晰。后来维护的人觉得“不能让状态机类太庞大”每加一种状态流转就新建一个类、注册一个扩展点。半年之后这个扩展点机制本身比它要解决的业务问题还复杂新来的同事光理解这套扩展机制就要花一周。设计原则本来是为了让代码更贴近业务变化但当它们被“禁词化”——字面上照搬、不考虑上下文——就会走向反面。真实世界里的设计原则每条都有它的适用边界和代价。单一职责强调的是“改变的原因”要单一不是“类的数量”要单薄。开闭原则的精髓是多态应对变化但前提是变化真的会发生而且方向可预判。没有这些前提的教条应用就是在制造套娃。所以每当你准备用某个原则来指导加层时先问这个设计遵循原则之后是让代码更容易改了还是让代码更难懂了原则是工具不是目的。3. 什么时候真的需要层次分界线的判断方法3.1 抽象的理由必须来自变化点说了这么多“不要乱分层”那到底什么时候该分层我自己的核心判断依据是变化点。意思是只有当你能指出一个具体的变化点而且这个变化点在未来一到两个迭代内真的有概率触发时才值得为它建立抽象层。变化点通常有两类。一类是“多个真实实现”。现在就要接入两个以上的数据源比如订单既来自自营系统也来自第三方同步系统二者的数据结构和处理逻辑有明显差异。这时候建立一个OrderSource接口让各实现自己处理差异业务侧统一调用这是合理的抽象。另一类是“重要的外部边界”。比如系统要接入一个不稳定或不规范的外部系统你需要在边界处做防腐处理把外部结构转成内部模型阻止外部变化影响核心逻辑。这种边界层的价值是立刻能看到的外部接口变了你只需要改边界层一处。判断的时候可以参考下面这几个问题这个层是否屏蔽了一个真实的、具体的差异如果不建这个层这个差异会渗透到多少处代码这个层自身是否稳定会不会因为变化而频繁修改去掉这个层调用方是否仍然清晰易懂如果答案偏向“没有具体差异、纯粹是规范驱动”那这层就不该存在。3.2 领域边界与技术边界要用在不同位置分层还有一个常见的混淆把业务分层和技术分层混为一谈。实际上这两种边界应该放在不同的位置解决的问题也不同。业务分层解决的是“业务规则的表达和复用”。比如一个订单提交的校验逻辑涉及库存、优惠、风控多处业务联动你把它收敛到领域层让Controller或者入口只做流程编排这是有意义的。因为业务规则本身上下凝聚、内部复杂统一收口后后期改规则时只有一个地方需要改。技术分层解决的是“基础设施与业务解耦”。比如你希望业务代码不直接依赖数据库、消息队列、文件存储的具体实现。你在业务和基础设施之间架一层仓储Repository接口业务只依赖接口底层实现可以替换。这种分层的价值在于基础设施变化时业务代码不感知。关键区别在于业务分层的变化点来自业务领域内部技术分层的变化点来自外部基础设施。两种变化点都真实存在所以两种分层都有合理依据。但如果一个层既没有承载领域逻辑又没有屏蔽基础设施变化它就纯粹是在“搭架子”没有实际意义。我见过很多代码库Service层声称承载业务逻辑但真正往下看里面的业务逻辑非常稀薄大量方法就是查一个表、返回一个VO。这种层既没有成为业务规则的收口点也没有成为技术边界的隔离点它只是一个物理存在、逻辑空转的“层次占位符”。3.3 三层视角按“变化的频率”和“变化的范围”分层最后分享一个我个人比较常用的分层视角不复杂就是按两个维度来观察代码变化的频率和变化的范围。变化的频率指的是某段代码隔多久会改一次。经常改的业务校验和几乎不变的数据库表结构映射它们的变化频率完全不同。变化的范围指的是一个改动会影响多少调用方。影响面大的层要谨慎设计边界影响面小的层可以适当粗糙。如果把代码按这两个维度看真正需要分离的是那些“变化频率不同”的部分。比如订单金额计算逻辑经常变优惠规则、定价策略而订单记录的存储方式很少变。你把常变的计算逻辑和少变的存储逻辑绑在一个类里每次金额规则变了都要动存储相关代码这种耦合才真正需要拆层。反之如果两部分的代码变动频率和范围都差不多那拆不拆层意义不大。拆了反而增加阅读和调用成本。所以每次有人问我“这两块代码要不要分开放”我的回答总是先看看它们各自的改动节奏。如果总是同进退放一起是简化如果各自改变才考虑隔离。这个视角的好处是它不依赖任何教条只依赖你对自己业务代码的观察而且非常直观。4. 动手简化一个重构实录4.1 重构前三层接口六步跳转的订单查询为了看得更清楚我拿一个真实的简化案例来走一遍。这是以前接手的一个订单查询接口功能很简单根据订单ID查到订单主记录连同明细一起返回给前端。重构之前的调用链是OrderController.getOrderDetail - OrderService.getOrderDetail - OrderManager.getOrderDetail - OrderHandler.queryOrder - OrderRepository.findOrderById - OrderMapper.selectOrder每条链路里还有对应的接口和实现类OrderService接口加OrderServiceImpl实现OrderManager接口加OrderManagerImpl实现OrderHandler接口加OrderHandlerImpl实现。每个类里的代码大同小异大部分方法就是调下一层的方法返回结果有些方法顺手加了个空注释“// 待补充业务逻辑”。问题在哪里非常明显。第一调用链六跳其中四跳没有任何业务逻辑纯粹是透传。第二任何字段的增删要改动从Controller到Mapper的结构体定义传参对象在很多层之间被转换、复制光看字段名对不上要花半天。第三新手接手第一周时间全花在搞清楚“我该往哪一层加代码”上而不是理解业务本身。第四没有一处提供额外价值——没有缓存没有监控埋点没有权限校验没有业务规则什么都没有。4.2 重构后保留哪些、合并哪些、删除哪些重构的目标不是“把层次减少到没有”而是“让每一层都回答一个问题”。我最后确定的方案是三层OrderController入口参数校验 HTTP语义处理 - OrderQueryService业务编排 - OrderRepository数据访问含基础缓存注意这里发生了什么。删掉了OrderManager和OrderHandler这两层透传因为它们不承担任何独特职责。保留了OrderRepository因为它确实承担了数据访问的封装而且未来如果要把订单存储从MySQL迁移到分库分表或者加缓存只动这一层。保留了OrderQueryService因为查询订单涉及到“订单主记录、明细记录、可能还有优惠记录”的组装这些数据的获取与拼装逻辑确实有业务味道值得单独收口。至于Controller它不需要接口抽象只有一个实现直接类引用即可。等未来真出现多个Controller入口需要复用这套查询逻辑时再考虑是否提升方法。重构后的调用链只有三层每一层的职责都清楚任何人接手这个模块读一遍三个类就知道整个查询流程。重构带来的实际效果单次需求改动涉及的文件从九处降到了两到三处新同事上手时间从一周缩短到两天代码量减少了接近五成。最关键的是当业务真正要加新的东西时——后来确实加了“订单附带发票信息”——我们知道该改哪里不需要在六层结构里猜来猜去。4.3 简化之后如何防止回潮重构完不等于一劳永逸代码架构有一种“熵增”倾向过几个月又会有人为了某种“规范”往上加层。我的经验是三管齐下。第一把简化原则写进团队约定。我们团队现在的代码评审Checklist里有三条硬性问题你新增的这个层/接口当前有几个实现它是否屏蔽了一个具体的差异删掉它调用方代码会怎样如果三个问题都没有令人满意的答案评审原则上不通过。第二给关键链路画一张“架构说明图”。就一张白底黑字的调用链图钉在WIKI首页任何人要加层先看图你打算插入在哪两个节点之间理由是什么如果理由说不通就往回走。第三定期做“复杂度巡检”。我在团队里每个月会对几个核心模块做一次调用链长度统计超过一定阈值的比如查询链路超过四跳就约上负责人聊一聊看看能不能合并。把这件事常态化之后代码库的层次膨胀速度明显降下来了。还有一个小技巧每当代码评审里有人喊“万一以后要扩展”我们就要求他把“以后”具体化——什么时候什么场景扩展什么如果说不出来那这个问题就暂时不存在不值得现在花费结构成本。5. 与团队站在一起让“简化”成为可沟通的共识5.1 评审会上怎么顶住“万一以后要扩展”的质疑提简化方案十有八九会遇到阻力最大的阻力就来自那句“万一以后要扩展”。怎么有理有据地沟通我有几句比较实用的回法。第一句“请列出你预见的那个扩展点。”这不是刁难是逼对方把模糊的担忧具体化。如果对方说以后要多接几个数据源那好讨论这个多数据源的差异具体在哪现在就为这个差异建层合理。如果对方说“我也说不清但多一层保险”那这一层就不该加——说不清的保险代价却清清楚楚现在每一个改动都要穿透这层。第二句“当前这层没有任何独特职责你写它本身也是有成本的。”绝大多数人只看到多一层“以后会省事”没看到多一层“现在就不方便”。你用一个具体的当前改动来演示比如给字段加个返回值现在要改几个文件如果由于这层存在导致改动翻倍就是最好证据。第三句“如果真到了那一天我们重构的成本分析过吗”很多人默认“现在不建接口以后要改代码成本很高”但实际上一个简单的系统里等到第二个实现来临时抽取接口往往只需要半天。而为了一个不存在的将来现在维护额外的层每个月都在付费。5.2 复杂度的可视化把“改动一个需求要动几处代码”量化推进简化光靠理念争论容易陷入各说各话我更推荐用数据说话。这年头大家的共识基础是“可测量的才算可靠”那我们就去量化复杂度。我个人在团队里用的核心指标有四个单需求改动文件数统计最近二十个需求平均每个需求改了哪些文件。套娃架构下这个数字通常很难看。调用链平均长度随便抽核心模块十个接口数一下从入口到数据库访问之间的方法调用跳数。超过五跳的基本都能找出透传层。圈复杂度与认知复杂度用静态分析工具跑一下关注那些“看起来每层都有方法但每个方法都极简单”的模块这种模块常常就是结构膨胀的重灾区。注释与代码的背离程度如果一个层里的注释比代码还多而且注释在描述“为什么要这层”那大概率说明这层存在本身就是需要解释的补偿行为。把这些指标贴到周会或者架构评审上比单纯说“我觉得这层没意义”要有说服力得多。很多反对者在看到“一个查询改动要动九个文件而其中七处只是转发”的统计数据后态度都会松动。5.3 小步重构与代码所有者的接受度有了数据和共识动手重构时也有一点要特别注意节奏。一次性大爆破重构风险高代码所有者也容易抗拒。我习惯的做法是“试点先行、小步快走”。挑一个业务相对独立、调用方少的模块先做一轮“去套娃”重构把改动前后的调用链、代码量、改动文件数对比表发出来让大家看到效果。有了一个成功样本后面再推其他模块的简化阻力会小很多。另外重构尽量不要侵入正在活跃迭代的业务模块。选那些“最近三个月都没怎么变更需求”的模块动手风险最小成果又容易让人一眼看懂。而且这类模块往往是最能反映纯结构问题的——没有业务需求在掩盖结构问题一层层透传就摆在眼前。如果代码所有者担心改动影响线上那就先用对比测试兜底重构前记录所有接口的输入输出样例重构后跑一遍对照逐字段比对。这一步做完绝大多数技术风险就已经消除了。剩下是人的接受度问题靠数据和耐心聊靠成功案例带动不靠强行push。最后聊两句心里话写了这么多其实核心就一句话架构该做的是“拦截复杂度”而不是“囤积层级”。判断一个架构好不好永远要看它能不能让你改代码更轻松而不是看它的结构图画得有多宏伟。我自己在项目里吃过太多套娃架构的亏。那种看着规范、实则处处透传的代码库表面上层次分明实际上每一次需求变更都变成穿越层层关卡。反而是那些看起来不那么“正规”、但每层都能回答“我为什么在这”的代码长期维护下来最省心。最后分享一个小技巧每次提交代码前翻翻自己这次加的类或接口问一句——这个新层次是让我这段代码更好懂还是更像一份“未来才用得上”的保险如果答案是后者那它大概率不该存在。这个自问的动作帮我避免了很多不必要的套娃。希望你也能试试。