在日常开发里,经常会听到有人说:这个做法是一个典型的 anti pattern。很多工程师大致知道这是个不太好的东西,但要精确回答 什么是计算机领域的 anti pattern,以及它和 坏代码、code smell、普通失误 有什么区别,就没有那么清晰了。
下面就从概念、历史、典型分类、具体例子、成因,到如何识别和避免,系统聊一聊 anti pattern。中间会穿插一个可运行的小例子,把抽象概念拉回到具体代码层面。
一、anti pattern 的精确定义是啥
anti pattern 这个术语,是 Andrew Koenig 在 1995 年提出的,用来呼应软件设计领域里已经很流行的 design pattern 概念 (Wikipedia)。后来 Brown 等人写的书 AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis 把这个概念系统化,不仅讨论代码设计里的 anti pattern,还扩展到了软件架构和项目管理 (ACM Digital Library)。
根据 Wikipedia 等资料,对 anti pattern 的经典定义可以总结成两点关键特征 (Wikipedia):
- 它是一种经常被采用的
惯用做法或常见解法,在表面上看起来合理、甚至一开始似乎运转良好,但随着时间推移,会带来比收益更大的负面影响。 - 针对它所解决的那类问题,已经存在一种被反复验证、文档化的更好做法,可以替代它。
也就是说,anti pattern 不是单纯的 错误 或 低级 bug。它往往来自有经验的人,出发点也可能是善意的,只不过在长期维护、扩展、协作的维度上,事实证明这条路非常坑。
很多文献还提到一个 三次原则:某种坏做法在不同项目里至少被观察到三次以上,并且有清晰描述和替代方案,才有资格被归纳为一个正式的 anti pattern (Wikipedia)。这样可以避免把个别偶然情况误当成通用规律。
如果用一句非常口语化的话来概括:
anti pattern 就是那种
一开始看上去挺聪明,大家也很爱用,但事后回头看,发现它其实是系统变烂的根源的惯用解法。
二、和 design pattern、code smell 的关系
理解 anti pattern,离不开另外两个常见概念:design pattern 和 code smell。
1. design pattern:经过验证的好套路
design pattern 这个概念,是 GoF 那本著名的 Design Patterns: Elements of Reusable Object-Oriented Software 系统普及开的 (Wikipedia)。它的本质是:
面向某类反复出现的设计问题,总结出一套可以复用的、高质量的解决方案模板。
例如:
Strategy模式:面向算法可以互换的场景,把变化的算法从稳定的调用方中抽离出来。Observer模式:面向一个状态变化,需要通知多个订阅者的场景。
这些模式经过长期实践检验,通常被视为 好习惯的沉淀。
2. anti pattern:坏套路的系统总结
anti pattern 在结构上,和 design pattern 很像,也会有:
- 名称(Name)
- 场景说明(Context)
- 问题描述(Problem)
- 典型结构(Structure)
- 后果(Consequences)
- 重构或替代方案(Refactored solution)
只不过,它描述的是 坏套路,重点是告诉你:这种看似常见又方便的做法,为什么长远看会带来灾难,以及你可以怎样 重构 掉它 (ACM Digital Library)。
从这个角度看,anti pattern 有点像 反向教材:通过记录失败经验,帮助大家识别危险信号。
3. code smell:症状 vs 诊断
code smell 被很多教材用来指代 代码中的坏味道,例如:
- 函数太长
- 过多的参数
- 重复代码
- 神秘数字(magic number)
这些通常是 症状,它自己并不一定就是一个完整的 anti pattern,但可以是 anti pattern 的表现形式。很多文章都会把 code smell 和 anti pattern 放在一起讨论 (blog.codacy.com)。
简单比喻一下:
- code smell 更像医生在体检时看到的
异常指标。 - anti pattern 更像
病的类型,包含成因、典型表现、危害和治疗方案。
三、anti pattern 的常见类别
软件工程里的 anti pattern 种类非常多,Wikipedia 上甚至有一整页的列表,从编程、设计、架构到方法论、配置管理等各个层面 (Wikipedia)。为了方便理解,可以粗略按层次分几类:
-
编程层面的 anti pattern
和具体代码结构直接相关,例如:Magic NumberCopy and Paste ProgrammingProgramming by PermutationError HidingLava Flow等。
-
面向对象设计 anti pattern
聚焦类和对象关系,例如:God ObjectPoltergeistCircular DependencyRefused Bequest等。
-
架构层面的 anti pattern
关注子系统之间的结构与依赖,例如:Big Ball of MudStovepipe SystemVendor Lock-inReinvent the Wheel等 (Wikipedia)。
-
过程与项目管理 anti pattern
不是代码本身,而是管理方式本身的问题,例如:Analysis ParalysisDeath by PlanningSmoke and MirrorsThrow It Over the Wall等 (Wikipedia)。
在不同层级的 anti pattern 背后,其实是同一类思维误区:只看眼前,不看演化;只看局部,不看整体。
四、几个经典 anti pattern 的直观解释
网上关于 anti pattern 的列表和讨论非常多,这里挑一些工程师日常最容易遇到的来聊聊 (Medium)。
1. God Object:什么都管的上帝类
现象
一个类负责所有事情:
- 维护大量全局状态
- 管理所有子模块
- 负责计算、持久化、日志、权限、配置
- 几乎所有地方都要
import它
代码看起来类似:
SystemManagerApplicationCoreGlobalService
问题
- 职责严重聚合,任何改动都容易牵一发动全身。
- 测试困难,没法单独验证某一部分逻辑。
- 团队协作困难,多人同时改同一个巨型类,冲突不断。
- 容易发生隐式的状态耦合,产生难以追踪的 bug。
更好的做法
- 按
单一职责原则拆分,区分领域对象、服务对象、基础设施。 - 使用
Facade、Strategy、Command等 design pattern,把变化和稳定的部分分层。
2. Spaghetti Code:意大利面式代码
现象
控制流像打结的意大利面:
- 大量嵌套的
if / else、switch goto或者隐式跳转(在现代语言里表现为混乱的回调地狱、难以追踪的异步调用链)- 函数之间相互调用,没有清晰层次
问题
- 阅读成本非常高,仅仅搞清楚
执行顺序就要画一下午流程图。 - 修改一个逻辑时,很难保证不会破坏别的路径。
- 新人极度痛苦:只敢
按类似写一个,进一步加重混乱。
更好的做法
- 引入清晰的层次结构:接口层、业务层、数据访问层。
- 对复杂分支使用
Strategy、State、Chain of Responsibility等模式。 - 避免过深的嵌套,通过早返回、拆分函数降低复杂度。
3. Golden Hammer:手里只有一把金锤子
Golden Hammer 形象地描述了一个心理:掌握了一种技术或模式之后,想在所有地方强行套用 (Wikipedia)。
典型表现
- 一个团队因为熟悉某个 ORM、某个框架、某种架构,就把所有问题都往这套东西上靠。
- 明明简单脚本就能搞定的任务,非要上一个超重型的微服务加消息队列。
- 明明只需要一个配置文件的定制,偏偏做成一整套 DSL 解析框架。
长期结果往往是:
- 系统过度复杂化。
- 学习成本陡增。
- 性能和资源浪费严重。
4. Cargo Cult Programming:货物崇拜式编程
Cargo Cult Programming 的灵感来自二战之后的南太平洋 cargo cult 现象:当地人看到飞机带来大量物资,却不了解背后的工业体系,于是模仿修机场、戴耳机、做塔台,希望飞机再降落 (Medium)。
映射到软件开发,就是:
- 引入某个看起来很
高级的 design pattern,但根本不了解动机和适用场景。 - 使用复杂的框架功能,但不了解其性能和边界条件。
- 在简单场景中,一味照抄网上的
最佳实践配置,而不考虑自己项目的特点。
这种行为短期看似 安全:毕竟是 网上说好的。长期结果却是:
- 团队对自己的系统缺乏真正理解。
- 出问题时,只会继续堆叠更多
魔法配置,形成更大的黑盒。
5. Big Ball of Mud:一团无法分辨的泥
Big Ball of Mud 描述的是那种缺乏清晰架构边界的系统:模块间界线模糊,依赖混乱,数据结构到处乱穿 (Wikipedia)。
常见成因有:
- 一开始原型写得比较随意,后续不断在此基础上堆功能。
- 缺乏总体架构设计和演化规划。
- 在业务快速变动压力下,为了交付频繁走捷径。
等系统变成 一团泥 之后,再想重构就异常艰难,因为:
- 没有清晰边界可拆。
- 没有可靠测试兜底。
- 任何修改都可能伤筋动骨。
五、一个简单可运行的 anti pattern 代码示例
说了这么多抽象概念,来看一个具体的代码例子。场景是非常常见的 支付处理 业务:
- 支持多种支付方式:信用卡、PayPal、数字货币等。
- 业务刚开始时支付方式不多,开发者习惯写一个大方法通过分支处理。
1. 典型的 switch type anti pattern
下面这个版本,就是很多项目早期常见的写法。它体现的是一种 switch type + 上帝方法 风格的 anti pattern。
# anti pattern 示例:所有支付逻辑都堆在一个方法里
class PaymentProcessor:
def __init__(self):
# 这里可能还会有各种共享状态、配置等
self.log_enabled = True
def pay(self, method: str, amount: float):
if method == 'credit_card':
self._log(f'Paying {amount} using credit card')
self._process_credit_card(amount)
elif method == 'paypal':
self._log(f'Paying {amount} using PayPal')
self._process_paypal(amount)
elif method == 'crypto':
self._log(f'Paying {amount} using crypto')
self._process_crypto(amount)
else:
raise ValueError(f'Unknown payment method: {method}')
def _log(self, message: str):
if self.log_enabled:
print(message)
def _process_credit_card(self, amount: float):
print(f'Calling credit card gateway, amount = {amount}')
# 省略各种具体实现
def _process_paypal(self, amount: float):
print(f'Calling PayPal api, amount = {amount}')
# 省略具体实现
def _process_crypto(self, amount: float):
print(f'Calling crypto node, amount = {amount}')
# 省略具体实现
if __name__ == '__main__':
processor = PaymentProcessor()
processor.pay('credit_card', 100.0)
processor.pay('paypal', 50.0)
processor.pay('crypto', 0.01)
这段代码是可以跑的,看上去也并不离谱。可是从设计角度看,它有典型的 anti pattern 特征:
-
违反开放封闭原则
每增加一种支付方式,都需要改
pay方法,在里面加一个新的分支,并新增对应的私有处理函数。调用方本来只想支持新方式,却不得不修改已有逻辑。 -
PaymentProcessor 逐渐演变为 God Object
长期下来,所有支付相关逻辑都堆在这个类里:
- 业务校验
- 风控
- 日志
- 重试
- 各种支付渠道
它慢慢变成一个
上帝类。 -
测试与扩展困难
想单独测试某个支付方式,很难在不触及其他逻辑的前提下进行。并且所有方式共享一堆隐式状态(例如日志、配置),耦合严重。
2. 使用 Strategy pattern 的改进版本
针对这个 anti pattern,在设计模式世界里有一个非常自然的替代解法:Strategy pattern。把 可变的支付方式 抽象成策略,让 PaymentProcessor 只负责协调与调用。
from abc import ABC, abstractmethod
from typing import Dict
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> None:
pass
class CreditCardPayment(PaymentStrategy):
def pay(self, amount: float) -> None:
print(f'Calling credit card gateway, amount = {amount}')
class PaypalPayment(PaymentStrategy):
def pay(self, amount: float) -> None:
print(f'Calling PayPal api, amount = {amount}')
class CryptoPayment(PaymentStrategy):
def pay(self, amount: float) -> None:
print(f'Calling crypto node, amount = {amount}')
class PaymentProcessor:
def __init__(self):
self.log_enabled = True
self._strategies: Dict[str, PaymentStrategy] = {}
def register_method(self, name: str, strategy: PaymentStrategy) -> None:
self._strategies[name] = strategy
def pay(self, method: str, amount: float) -> None:
strategy = self._strategies.get(method)
if strategy is None:
raise ValueError(f'Unknown payment method: {method}')
self._log(f'Paying {amount} using {method}')
strategy.pay(amount)
def _log(self, message: str) -> None:
if self.log_enabled:
print(message)
if __name__ == '__main__':
processor = PaymentProcessor()
processor.register_method('credit_card', CreditCardPayment())
processor.register_method('paypal', PaypalPayment())
processor.register_method('crypto', CryptoPayment())
processor.pay('credit_card', 100.0)
processor.pay('paypal', 50.0)
processor.pay('crypto', 0.01)
这个版本同样可以直接运行,但在可演化性上好很多:
- 新增一个支付方式,只需要实现一个新的
PaymentStrategy子类,再在初始化时注册,不必修改已有pay逻辑。 PaymentProcessor保持相对简单,只关注调度与日志。- 测试层面,可以单独对某个策略类做单元测试。
这个例子体现了 anti pattern 的一个典型特点:短期交付看,它是最直接、最快写完的;但系统演化到一定规模时,它会变成加速腐化的源头。这一点在很多关于 anti pattern 的文章和教材里都有类似的讨论 (blog.codacy.com)。
六、为什么 anti pattern 如此容易出现
很多 anti pattern 并不是 低水平程序员的错,反而经常出现在经验不算少的团队里。常见诱因大致有几类:
1. 局部最优导致的全局失衡
人在做决策时,往往先看眼前的局部收益,例如:
- 当前需求能不能快速上线
- 当前这次重构能不能按期完成
- 当前这个问题能不能先绕过去
在这种心态下:
临时兜底的逻辑没有被及时清理,形成Lava Flow。- 为了兼容一些特殊场景,增加了很多
if / else分支,却没有回过头重新整理抽象。 - 为了减轻某个子系统的压力,简单粗暴地把责任转嫁到调用方,形成奇怪的 API。
短期看都能自圆其说,长期叠加后,就演变成 anti pattern。
2. 对设计原则理解不完整
很多 design pattern 和架构规则,本身是有前提条件的。例如:
微服务需要有清晰的领域边界和自动化运维能力。CQRS适合高并发读写分离的场景,而不是所有系统。事件驱动需要团队对异步一致性有深刻理解。
当这些前提不具备,直接照抄结构,很容易变成 形似而神不似 的 cargo cult,最后留下一个难以维护的复杂系统 (Medium)。
3. 项目压力与组织文化
许多 anti pattern 被系统化梳理之后,会发现背后是组织层面的压力和激励机制 (Wikipedia):
- 短期交付压力极大,导致反复
Fire Drill式救火。 - 管理层习惯于通过文档和计划来
控制一切,于是产生Analysis Paralysis、Death by Planning。 - 奖励
快速搞定需求,而不是改善系统健康。
在这样的环境下,哪怕团队成员知道某种做法是 anti pattern,也很难真正避免。
七、如何识别一个做法是不是 anti pattern
体验丰富的工程师在看代码或者架构图时,往往会本能觉得 这里有股味道。这背后其实可以总结出几条可操作的判断思路,和很多资料中的建议是一致的 (blog.codacy.com):
1. 这是不是一个 常见 做法
如果只是在一次性脚本或实验性原型中,用了点 不优雅 的小技巧,并不会自动升级为 anti pattern。要纳入 anti pattern 范畴,通常需要:
- 在多个项目、多个上下文里被反复采用。
- 被视为
默认做法或习以为常的套路。
如果你在团队中频繁看到某种写法,例如每个服务都在用 copy paste 的方式扩展,或者所有异常处理都被 try / except: pass 吞掉,既不好好记录,也不向上抛出,那就非常值得警惕。
2. 它是不是 看起来很合理
很多 anti pattern 并不是显然错误的。相反,它一开始往往可以通过以下方式自洽:
- 能解释
为什么我当时要这么做。 - 能在短期内解决迫在眉睫的问题。
- 还能从某个维度(性能、交付速度、局部简洁)展示出优势。
例如:
- 大家都很忙,一个巨大的
Manager类负责一切协调也挺方便。 - 部署流程复杂,干脆写一个万能脚本,用大量条件分支来兜底所有边缘情况。
- 平台不稳定,为了防止报错吓到用户,就把报错都隐藏,给用户返回一个模糊错误信息。
当一个做法 自说自话 得很通顺,却又莫名其妙让系统变得越来越难搞,就很有可能是 anti pattern。
3. 是否存在被充分验证的替代方案
这是把 anti pattern 与单纯 失败尝试 区分开的关键:对于其试图解决的问题,是否已经有更好的、公认的解决方式。
例如:
- 对于
God Object,可以使用分层架构、领域驱动设计、多个服务职责分离等成熟方式来替代。 - 对于
Magic Number,可以通过常量定义、类型封装来表达含义。 - 对于
Copy and Paste Programming,可以通过抽象公共模块、提取方法、使用库来减少重复。
如果一个看起来很糟糕的做法,确实暂时没有更好的替代方式,那就更像是 无奈选择 而不是 anti pattern。
4. 长期后果是否大于短期收益
这个问题可以通过回顾式的方式来思考:假设项目运行两三年之后,这个决策会带来怎样的累积影响?
- 是否显著增加系统复杂度?
- 是否阻碍新功能的引入?
- 是否让排查问题的成本急剧上升?
- 是否导致团队新人几乎无法上手?
如果答案偏向 是,就需要谨慎对待。
八、如何在团队中系统地处理 anti pattern
anti pattern 本质上是 组织经验 的一种沉淀,所以处理它往往不能只靠单兵作战。很多工程实践和文献给出了一些相对系统的思路 (Baeldung on Kotlin)。
1. 建立团队自己的 anti pattern 清单
可以参考 Wikipedia 的列表和一些专业文章,把常见的 anti pattern 结合自己项目的实际情况,整理成内部文档 (Wikipedia)。例如:
- 代码层面的内部清单:
禁止随意静态单例、避免巨型 Manager 类、避免万能 Util 模块等。 - 架构层面的清单:
慎用远程调用链过长的同步依赖、避免服务间双向依赖等。 - 过程层面的清单:
避免需求文档反复拉锯但迟迟不做 MVP等。
这份清单不必一开始就做得完美,重要的是能持续更新,从团队的真实经验出发。
2. 把 反例学习 融入 code review 和设计评审
很多团队的 review 文化只强调 找 bug,但对于 潜在的 anti pattern 并不敏感。一个更健康的方式是:
- 在 review 中,如果发现某种可疑结构,不只是说
这样不好,而是尝试命名它:这是不是某种God Object、Spaghetti Code、Cargo Cult的苗头。 - 给出可替代的设计方案,并解释其适用场景。
久而久之,团队成员的 模式识别能力 会增强,看到类似结构时就会自动警觉。
3. 对重要重构进行 前后对比 的知识沉淀
当你把一个典型的 anti pattern 重构掉时,其实非常适合做一个 前后对照 的小文档:
- 之前的结构长什么样,为什么是 anti pattern。
- 重构后的结构是什么,具体如何落地。
- 在性能、可测试性、可维护性上的变化。
这种 具体案例 比抽象原则更有说服力,也更容易被新成员吸收。
4. 在架构决策记录里标记 风险模式
很多团队会维护 Architecture Decision Record (ADR)。在其中一栏,可以专门记录:
- 采用某种方案时,有哪些已知 anti pattern 的风险。
- 将来如果这些风险加剧,可能采用哪些替代路径。
这样可以在一开始就让大家对潜在陷阱有共同预期,避免几年后回顾时只剩下模糊记忆。
5. 借助工具识别 code smell,配合人工分析 anti pattern
静态分析工具擅长识别 code smell:
- 过长的函数
- 未使用的变量
- 重复代码片段
但是否构成 anti pattern,仍然需要工程师结合上下文判断。可以采用的做法是:
- 把工具扫描结果作为
候选问题列表。 - 在技术例会上挑一些典型例子,讨论是否可以归为某种 anti pattern。
- 如果答案是肯定的,就把这次讨论沉淀到团队清单中。
九、anti pattern 思维对个人成长的意义
对于个人来说,理解 anti pattern 有几个很实际的好处:
-
提高对
长期成本的敏感度
每当你在代码里写下一行暂时这样先搞定的逻辑,脑子里会多响一遍警钟:这会不会是某种 anti pattern 的胚胎。 -
提高对
结构的观察力
阅读他人代码和系统架构时,可以从模式的角度去观察,而不是只看局部实现。久而久之,会形成自己的问题分类体系。 -
帮助与非技术角色沟通技术债
anti pattern 通常伴随可描述的长期后果,你可以用更具象的语言向产品、管理者解释:为什么现在这次需求要多花一点时间做结构性改动,否则未来会付出什么代价。 -
反向理解 design pattern 的边界
当你看到一些被误用的 design pattern演变成 anti pattern 时,就更容易理解这些模式的前提条件和适用场景。
十、简单概括一下
把上面一大堆内容压缩成几句话,可以得到这样一个图景:
- anti pattern 是一种
常见又看似合理的解决方案,但事实证明它会在长期内损害系统健康,并且针对同类问题已经存在更好的替代方案。 - 它既不是简单的低级错误,也不是抽象的哲学命题,而是可以被命名、被描述、被重构的
坏套路。 - 在软件工程实践中,anti pattern 遍布代码、设计、架构和项目管理的各个层面。
- 学会识别和命名 anti pattern,既有助于提升个人的设计能力,也有助于团队把
踩过的坑变成可传承的知识。
在真实项目中,没有哪个团队可以完全避免 anti pattern。更现实的目标,是在它们刚刚长出苗头的时候就尽早识别、及时修剪,而不是等到系统变成 Big Ball of Mud 之后,再痛苦地讨论要不要 推倒重来。
如果你愿意,后面可以一起结合你正在做的某个系统,实地画一画架构图,把其中可能的 anti pattern 一个个挑出来,对应地设计重构路线,这种 带项目的练习 会比任何抽象教程都来得扎实。
10万+

被折叠的 条评论
为什么被折叠?



