Bean 化滥用:你写代码一股 Spring 的腐烂味!
这两天梳理代码的时候,被一个模块的设计整破防了!简单来说,有一个处理标签的模块,业务逻辑是这样的:根据标签类型,给实体对象的不同字段赋值。翻译成代码,就是一个 if-else:
if (type == TAG_A) {
entity.setFieldA(value);
} else if (type == TAG_B) {
entity.setFieldB(value);
} else if (type == TAG_C) {
entity.setFieldC(value);
}
但实际的代码长这样:
TagHandler (interface) ← 四个方法:判断类型、处理标签、前置钩子、后置钩子
AbstractTagHandler (abstract) ← 模板方法:日志、状态记录
TagAHandler (@Component) ← 三行:if type == A, entity.setFieldA(value)
TagBHandler (@Component) ← 三行
...
TagGHandler (@Component) ← 七八个,全是三行代码
TagHandlerChain (@Component) ← 注入所有 Handler,遍历找到能处理的那个
五个层次,十个文件,追一个标签处理逻辑要跳六次。而它解决的问题,是一个 if-else。
这不是个例。这是一种在 Java 后端工程里极其普遍的坏味道:用复杂的结构去描述一个简单的问题,而不是解决它。
无脑 Bean 化的坏味道,这种惯性是怎么流行起来的
面试驱动?Spring 惯性?代码形式主义?这种做法流行起来,不是一个原因,是几股力量同时拉扯的结果。
面试驱动的设计,坏味道代码的万恶之源
"讲一个你用过设计模式的例子",面试的八股文经常会问到这些。而“设计模式”又是程序员在工作中少有的可以用到的高级技巧。所以为了有故事可讲,工程师会在日常编码里刻意使用设计模式。但问题在于,很多人知道策略模式长什么样,但不清楚它解决的是什么问题。于是用起来的方式是"把代码的形状改成策略模式的形状",而不是"判断这里的问题是不是策略模式要解决的问题"。形式对了,动机错了。
Spring 的惯性,冲击了了生命周期管理的习惯
由于 Spring 框架 IOC 的设计思想,注入一个 @Component 的摩擦太低了,低到它变成了一个无意识的动作。手写 new 的时候,你至少会停顿一下,简单想想这个对象的生命周期是什么,谁来持有它。但加一个注解变成 Bean 后,容器帮你管了一切。这种便利性本身就是一种诱导,让人倾向于把所有东西都托管进去,而不去思考是否有必要。
预防性编程走偏,和空气斗智斗勇
"万一以后业务复杂了?",这是不是你经常会想的问题。工程师很容易想象业务变复杂的场景,但很少想象业务保持简单、或者以完全不同的方式变化的场景。结果是:业务一直简单,代码一直复杂。YAGNI 原则说的就是这个:不要为你想象中的需求写代码,只为你现在确定的需求写代码。扩展性是设计出来的,不是提前堆砌出来的。
代码审查的形式主义,为用而用的设计模式
"这里 if-else 太多了,建议用策略模式优化",遇到这种评审意见,你是不是下意识地就觉得该优化?这种 review 意见的判断标准是代码的形状,而不是代码的问题,是典型的“代码形式主义”。if-else 本身不是坏味道,if-else 里的逻辑重复、分支无法独立测试、条件表达不清晰,这些才是坏味道。更隐蔽的是,这种 review 文化会训练被 review 的人用同样的标准看代码,一届一届传下去,整个团队都在用"形状"而不是"问题"来评判代码质量。
设计与落地之间的鸿沟,Bean 是最万能的翻译
架构图上的一个方框"TagHandler",在设计者脑子里代表的是"一个处理标签的职责边界",是一个概念。但落地的工程师拿到这个图,翻译成了"一个 interface + 若干 @Component 实现类",是一个具体的结构。这个跳跃没有人负责。设计评审讨论的是"要不要拆 Handler",但没有人讨论"Handler 应该是 Bean 还是普通类"、"分发逻辑应该在容器里还是在一个 switch 里"。这些实现层面的决策被默认留给了执行的人,而执行的人缺乏判断依据,只能套用熟悉的模式填空。设计给了方向,但没有给约束,执行在没有约束的空间里,自然会用惯性填满它。
这五股力量各自独立,有时候单独触发,有时候几条叠加,结果都是同一种坏味道。没有一个统一的根因,也没有一个简单的解法。
你可能意识不到,滥用 Bean 的危害有多大!
可读性的严重下降
单个 Handler 三四行,看起来很简单。但理解这段逻辑的成本不是七八个 Handler 的成本之和,而是要先理解整个体系的结构:接口是什么、模板做了什么、分发逻辑在哪里——才能开始读具体的实现。这是一个固定的"入场费",不管你只是想改一个字段的 set 逻辑,还是要新增一个标签类型,都要先付这个费用。逻辑越简单,这个入场费的相对成本就越高。
修改的仪式感虚假膨胀
新增一个标签类型,原本是在一个方法里加一个 else if,五分钟的事。现在变成:新建一个文件、实现接口的四个方法、加 @Component、确认责任链能路由到它、补测试。每一步单独看都不复杂,但合在一起形成了一种心理负担——改这么多地方,会不会漏掉什么?这种不确定感会让工程师在改动前犹豫,在改动后不安。小需求的交付摩擦被放大了。
测试的假象:可测的不用测,该测的测不动
这套结构看起来很好测——每个 Handler 都可以单独测,接口清晰,职责分明。但你测的是什么?TagAHandler.handle() 里就一行 entity.setFieldA(value),这个测试在验证 Java 的赋值语句能正常工作。真正需要测试的是整体行为——给定一个 type,最终 entity 的状态是否正确——但这个测试因为结构的复杂性变得难写,需要 mock 整个责任链的上下文。结构越复杂,集成测试的成本越高,于是大家只写单元测试,只测那些没有价值的叶子节点。
最严重的危害:认知盲区型技术债。
这是最隐蔽的危害。当一个简单的 if-else 被包装成了一套精心设计的 Handler 体系,它在视觉上和认知上都"看起来已经被好好设计过了"。读者的第一反应不是"这里的逻辑是什么",而是"这里有一个设计",于是开始理解这个设计,而不是质疑它。追问的起点从"这个问题是否被正确解决了"变成了"这个设计我是否看懂了"。
这个债务有一个特殊的演化过程。设计的时候,设计者是清醒的,他知道自己做了什么决策。这个阶段"没有问题",或者说问题被设计者的上下文消化掉了,没有外显。交接之后,接手的人看到一套完整的 Handler 体系,合理地假设这是经过思考的,进入"理解模式"而不是"审视模式"。债务在这个阶段开始积累,但没有人感知到。问题暴露的时候,往往是一个外部触发点:新人觉得哪里不对劲但说不清楚,或者另一个团队接手之后发现改不动。这时候代码已经在这个模式上继续生长了很久,Handler 从七八个变成了十几个,依赖这套结构的上层逻辑也已经很多了。
普通技术债是"知道有问题但没改",这种债是"不知道有问题所以没改"。前者是意志问题,后者是认知问题。认知问题更难通过流程和规范来解决,因为它连被提上议程的机会都没有。
怎么判断一段逻辑是否需要 Bean 化和抽象
不是事后诊断,而是写代码时的决策依据。对一段逻辑,问这三个问题:
它有没有状态? 有状态的逻辑——持有资源、维护上下文、生命周期需要被管理——Bean 化是合理的,容器在帮你做生命周期管理。无状态的逻辑就是纯粹的数据变换,输入输出,没有副作用。这种逻辑不需要 Bean,不需要注入,一个静态方法或者局部方法就够了。标签处理那个例子,每个 Handler 就是一个 set,无状态,Bean 化没有任何收益。
它有没有跨域业务编排? 单个域内的逻辑,内聚在一起就好。但如果一段逻辑需要协调多个域——比如下单同时触发库存、营销、通知——这里有真实的编排职责,值得一个独立的对象来承载。这条判断区分了"逻辑的实现者"和"逻辑的编排者",编排者值得被显式地表达出来,因为它承载的是业务流程的骨架。
它有没有分层封装的架构需求? 即使逻辑本身不复杂,如果架构上需要在这里插入一个横切层——AOP、拦截、监控、权限——那这个中间层是有存在价值的,它服务的是架构约束,不是业务逻辑。这条也划定了一个边界:分层是为了架构目的,不是为了"看起来有层次"。如果一个中间层没有任何横切逻辑,只是透传调用,它就是多余的。
如果三条都不满足,就用最简单的方式写。需要引入结构的一方要能说清楚理由,而不是默认就上 Handler 体系,然后说不清楚为什么。
给所有 Spring 框架程序员的开发建议
看到这里,你也应该知道 Spring 的坏味道是什么样,会带来哪些严重危害。不过,哪怕你知道这些,长期写 Spring 的习惯也很难让你修正 Bean 滥用的坏习惯,所以,这里有一些实在的建议。
写之前先做诊断。 对照上面三条判断:有没有状态,有没有跨域编排,有没有分层需求。三条都没有,就用最简单的方式写——私有方法、静态方法、局部 if-else。把举证责任反过来:默认是简单实现,需要引入结构的一方要能说清楚理由。
让复杂度显式可见,不要用结构掩盖它。 如果一段逻辑确实复杂,不要用结构去掩盖它,要让它的复杂度直接暴露出来。一个长一点的方法,加上清晰的注释说明每个阶段在做什么,比七八个 Handler 分散在不同文件里更诚实,也更容易被后来者审视和质疑。复杂度可见的好处是它会产生压力——一个 200 行的方法放在那里,所有人都知道这里需要被处理。但七八个各自只有三行的 Handler,看起来干净整洁,没有人会觉得这里有问题。
重构的时机是痛点出现的时候。 当你发现同一段逻辑在两个地方重复了,重构。当你发现一个分支复杂到需要独立测试,重构。当你发现修改一个需求要动五个文件,重构。重构的动作要小而精准——只解决当前这个具体的痛点,不顺手把"将来可能有问题"的地方也一起改了。每次重构之后,代码应该比之前更简单,而不是更复杂。
code review 时,问动机而不是看形状。 看到 Handler 体系,不要问"这个设计合不合理",要问"这里为什么需要这个结构"。让写代码的人说清楚:这段逻辑有状态吗,有跨域编排吗,有分层需求吗?说得清楚,结构就是合理的。说不清楚,就是过度设计。这个问法把判断标准从审美变成了逻辑,不容易掰扯不清。
杜绝 Spring 写 Bean 成瘾的惯性。 具体的破法是建立一个默认反应:写一个新类之前,先问它是不是需要被容器管理。不是"加 @Component 有什么问题",而是"不加 @Component 能不能解决问题"。能,就不加。这个习惯需要刻意练习,因为 @Component 的摩擦太低了,低到它变成了一个无意识的动作。把它变回一个有意识的决策,是第一步。
保持简单实现,持续重构。 这个思想背后有一个心理建设要做:允许自己现在写"不够好"的代码。很多过度设计来自于一种焦虑——"如果我现在不把扩展性设计好,将来改起来会很痛"。但这个焦虑本身就是问题的来源。真正的自信是:我现在用最简单的方式实现,当复杂度真的到来的时候,我有能力重构它。持续重构不是承认失败,是一种主动的、有节奏的代码健康管理。
总结
最后回到那个标签处理的例子。那套 Handler 体系里,模板方法的日志和状态管理是有价值的——那是真实的公共逻辑,值得抽象。但它不需要七八个 Bean 来承载,一个带有私有方法的 Service 加上清晰的 if-else 就够了。为了承载 20% 有价值的抽象,建造了 80% 不必要的结构,这个比例本身就说明了问题。
复杂的思考可以落地成简单的实现。真正想清楚了一个问题的人,往往写出来的代码更少,不是更多。