news 2026/10/4 8:53:32

架构不是堆层次:判断该不该加一层的实用标准

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
架构不是堆层次:判断该不该加一层的实用标准

一次评审会上,年轻同事指着一份设计文档问我:“这个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的getUserNameById,Service里没有任何业务逻辑,直接调UserMapper的getUserNameById,Mapper查数据库返回。这三层代码,从方法名到参数到返回值,几乎完全一样。为什么要分成三层?因为“规范”要求这么分层。这就是对称美驱动的设计——每一层都必须存在,否则看起来“不完整”。

这种同构层的危害,除了前面说的心智负担之外,还有一个不易察觉的点:它会掩盖真正的职责边界。当一个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。

最后聊两句心里话

写了这么多,其实核心就一句话:架构该做的是“拦截复杂度”,而不是“囤积层级”。判断一个架构好不好,永远要看它能不能让你改代码更轻松,而不是看它的结构图画得有多宏伟。

我自己在项目里吃过太多套娃架构的亏。那种看着规范、实则处处透传的代码库,表面上层次分明,实际上每一次需求变更都变成穿越层层关卡。反而是那些看起来不那么“正规”、但每层都能回答“我为什么在这”的代码,长期维护下来最省心。

最后分享一个小技巧:每次提交代码前,翻翻自己这次加的类或接口,问一句——这个新层次,是让我这段代码更好懂,还是更像一份“未来才用得上”的保险?如果答案是后者,那它大概率不该存在。这个自问的动作,帮我避免了很多不必要的套娃。希望你也能试试。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 8:47:45

工业数据存储不掉电:PIC18搭配SPI MRAM的实战方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 8:47:00

Python量化回测平台对比:聚宽、米筐、优矿和掘金怎样保存实验

Python量化回测平台可以比较聚宽、米筐、优矿和掘金量化。四款候选都需要代码、数据、参数和输出,但网页项目、本地产品与开发终端的保存方式不同。比较时不直接看收益高低,而是检查三个月后能否用同一份资料重建实验。最小实验包包含环境版本、策略代码…

作者头像 李华
网站建设 2026/10/4 8:44:53

OpenShell:整合PowerShell与WSL的Windows终端增效实战

说实话,我一开始看到“OpenShell”这个名字,以为又是一个 Windows 终端的换肤工具。毕竟这年头,给终端加个背景图、调个透明度,就能自称“生产力神器”的项目太多了。但真正装完、配置好、用了两周之后,我想说&#xf…

作者头像 李华
网站建设 2026/10/4 8:44:25

金融机构接连入驻WorkBuddy,争的不是多一个Skill,是下一个高频入口

自腾讯9月初发布WorkBuddy金融版,面向金融机构推出AI智能工作台后,券商陆续入驻WorkBuddy,角力下一个流量入口。继腾讯发布WorkBuddy金融版后,广发证券、东方财富、兴业证券、中信建投相继入驻WorkBuddy。四家机构分别从对外投研专…

作者头像 李华
网站建设 2026/10/4 8:42:51

Python新手练习路线:从环境搭建到小项目实战避坑指南

如果你最近打算学 Python,大概率会收到同一个建议:多练习。但真正的问题马上就来——练什么?按什么顺序练?装好解释器之后第一步到底干嘛?我自己刚入门的时候就是卡在这,看了一堆教程,代码抄了一…

作者头像 李华