理解计算机领域的 anti pattern:那些看起来没错,却把系统慢慢拖垮的做法

在日常开发里,经常会听到有人说:这个做法是一个典型的 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):

  1. 它是一种经常被采用的 惯用做法常见解法,在表面上看起来合理、甚至一开始似乎运转良好,但随着时间推移,会带来比收益更大的负面影响。
  2. 针对它所解决的那类问题,已经存在一种被反复验证、文档化的更好做法,可以替代它。

也就是说,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)。为了方便理解,可以粗略按层次分几类:

  1. 编程层面的 anti pattern
    和具体代码结构直接相关,例如:

    • Magic Number
    • Copy and Paste Programming
    • Programming by Permutation
    • Error Hiding
    • Lava Flow 等。
  2. 面向对象设计 anti pattern
    聚焦类和对象关系,例如:

    • God Object
    • Poltergeist
    • Circular Dependency
    • Refused Bequest 等。
  3. 架构层面的 anti pattern
    关注子系统之间的结构与依赖,例如:

    • Big Ball of Mud
    • Stovepipe System
    • Vendor Lock-in
    • Reinvent the Wheel 等 (Wikipedia)。
  4. 过程与项目管理 anti pattern
    不是代码本身,而是管理方式本身的问题,例如:

    • Analysis Paralysis
    • Death by Planning
    • Smoke and Mirrors
    • Throw It Over the Wall 等 (Wikipedia)。

在不同层级的 anti pattern 背后,其实是同一类思维误区:只看眼前,不看演化;只看局部,不看整体。


四、几个经典 anti pattern 的直观解释

网上关于 anti pattern 的列表和讨论非常多,这里挑一些工程师日常最容易遇到的来聊聊 (Medium)。

1. God Object:什么都管的上帝类

现象

一个类负责所有事情:

  • 维护大量全局状态
  • 管理所有子模块
  • 负责计算、持久化、日志、权限、配置
  • 几乎所有地方都要 import

代码看起来类似:

  • SystemManager
  • ApplicationCore
  • GlobalService

问题

  • 职责严重聚合,任何改动都容易牵一发动全身。
  • 测试困难,没法单独验证某一部分逻辑。
  • 团队协作困难,多人同时改同一个巨型类,冲突不断。
  • 容易发生隐式的状态耦合,产生难以追踪的 bug。

更好的做法

  • 单一职责原则 拆分,区分 领域对象服务对象基础设施
  • 使用 FacadeStrategyCommand 等 design pattern,把变化和稳定的部分分层。

2. Spaghetti Code:意大利面式代码

现象

控制流像打结的意大利面:

  • 大量嵌套的 if / elseswitch
  • goto 或者隐式跳转(在现代语言里表现为混乱的回调地狱、难以追踪的异步调用链)
  • 函数之间相互调用,没有清晰层次

问题

  • 阅读成本非常高,仅仅搞清楚 执行顺序 就要画一下午流程图。
  • 修改一个逻辑时,很难保证不会破坏别的路径。
  • 新人极度痛苦:只敢 按类似写一个,进一步加重混乱。

更好的做法

  • 引入清晰的层次结构:接口层、业务层、数据访问层。
  • 对复杂分支使用 StrategyStateChain 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 特征:

  1. 违反开放封闭原则

    每增加一种支付方式,都需要改 pay 方法,在里面加一个新的分支,并新增对应的私有处理函数。调用方本来只想支持新方式,却不得不修改已有逻辑。

  2. PaymentProcessor 逐渐演变为 God Object

    长期下来,所有支付相关逻辑都堆在这个类里:

    • 业务校验
    • 风控
    • 日志
    • 重试
    • 各种支付渠道

    它慢慢变成一个 上帝类

  3. 测试与扩展困难

    想单独测试某个支付方式,很难在不触及其他逻辑的前提下进行。并且所有方式共享一堆隐式状态(例如日志、配置),耦合严重。

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 ParalysisDeath 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 ObjectSpaghetti CodeCargo 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 有几个很实际的好处:

  1. 提高对 长期成本 的敏感度
    每当你在代码里写下一行 暂时这样先搞定 的逻辑,脑子里会多响一遍警钟:这会不会是某种 anti pattern 的胚胎。

  2. 提高对 结构 的观察力
    阅读他人代码和系统架构时,可以从 模式 的角度去观察,而不是只看局部实现。久而久之,会形成自己的 问题分类体系

  3. 帮助与非技术角色沟通技术债
    anti pattern 通常伴随可描述的长期后果,你可以用更具象的语言向产品、管理者解释:为什么现在这次需求要多花一点时间做 结构性改动,否则未来会付出什么代价。

  4. 反向理解 design pattern 的边界
    当你看到一些 被误用的 design pattern 演变成 anti pattern 时,就更容易理解这些模式的前提条件和适用场景。


十、简单概括一下

把上面一大堆内容压缩成几句话,可以得到这样一个图景:

  • anti pattern 是一种 常见又看似合理的解决方案,但事实证明它会在长期内损害系统健康,并且针对同类问题已经存在更好的替代方案。
  • 它既不是简单的低级错误,也不是抽象的哲学命题,而是可以被命名、被描述、被重构的 坏套路
  • 在软件工程实践中,anti pattern 遍布代码、设计、架构和项目管理的各个层面。
  • 学会识别和命名 anti pattern,既有助于提升个人的设计能力,也有助于团队把 踩过的坑 变成可传承的知识。

在真实项目中,没有哪个团队可以完全避免 anti pattern。更现实的目标,是在它们刚刚长出苗头的时候就尽早识别、及时修剪,而不是等到系统变成 Big Ball of Mud 之后,再痛苦地讨论要不要 推倒重来

如果你愿意,后面可以一起结合你正在做的某个系统,实地画一画架构图,把其中可能的 anti pattern 一个个挑出来,对应地设计重构路线,这种 带项目的练习 会比任何抽象教程都来得扎实。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

汪子熙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值