设计模式很少因为“错”而失败。更常见的是,我们在不合适的时机、出于不对的原因去套用它们,或者把它们当作替代品,回避给真实问题命名。通常,难点并不在于记住某个模式的存在,而在于判断你的代码此刻是否需要它,还是一个更简单的动作更合适。

这正是决策树有用的原因。它在你选择模式之前强制你多做一步自律:你要先描述你想消除的摩擦。
你是否在为对象创建越来越复杂而头疼?你是否在和组件之间或外部依赖的边界作斗争?抑或主要问题是行为在不同场景或时间里不断变化,导致你的代码里条件分支不停增加?
本文的目标是给你一小组问题,引导你走向一份短清单:几种更贴合你情境的模式。你仍然需要判断力,但会把更少的时间花在猜测上,把更多的时间用在做决定上。
为什么设计模式有用?
当模式能降低重复出现的成本时,它才值得被采用。在实践中,这些成本常常表现为:
- 改动需要触及太多文件
- 测试很慢或很脆,因为代码没有清晰的接缝
- 外部 API 渗入领域逻辑,到处散落“转换”代码
- 构造函数和初始化代码不断膨胀,有效组合变得不清晰
- 相同逻辑被复制,因为它没有一个稳定的归宿
错误在于把模式本身当作升级。这并不对。模式的意义在于:把为灵活性付出的代价,集中在可控的局部来支付,而不是在系统各处、反复支付。
三个问题走完这棵决策树
先问一个问题:痛点来自哪里?
再收窄范围:
- 痛点是否与创建对象有关?
- 痛点是否与对象如何组合在一起有关?
- 痛点是否与跨场景或随时间变化的行为有关?
它们分别对应创建型、结构型、行为型三类模式。你可以忽略这些类别本身;重要的是问题本身。
分支一:创建对象(Creational patterns)
当创建逻辑本身变成问题时使用此分支:参数过多、重复初始化、不清晰的默认值,或“这里该创建哪个实现?”的逻辑在代码库中四处蔓延。
第一步:你真的只需要一个实例吗?
如果你正要选择单例模式,请明确原因。“容易访问”不是强理由;它往往掩盖了依赖,让测试更难做。
当对象实际上是无状态的或“可安全共享”时(例如:只读配置快照、进程级日志包装器),单例可能是合理的。一旦它持有可变状态、请求上下文,或任何需要在测试间复位的东西,就会变得危险。

如果你想要的是受控的构造与显式的装配,依赖注入或一个小型应用容器往往比全局实例更耐用。
第二步:构造是否复杂或易被误用?
当构造函数不断累积可选参数,且配置组合开始重要时,建造者模式通常是最干净的选择。重点不在于链式调用的“美观”,而在于让对象创建显式化,并尽早校验。
# 没有建造者:可读性差且容易被误用
request = Request.new(url, method, headers, body, timeout, retry_count, cache, auth)
# 使用建造者:意图更清晰,更易校验
request = RequestBuilder.new
.url("https://api.example.com")
.method(:post)
.headers(auth_headers)
.timeout(2)
.build
建造者也便于暴露一小组“公认良好”的预设(例如:默认的重试策略),而不必强迫每个调用方去拼装一长串参数。

第三步:你是否在根据上下文选择实现?
当代码反复根据配置、文件类型、服务商、特性开关或环境来决定要实例化哪个具体类时,这个决策应该被集中管理。
- 工厂方法 适用于基类定义契约、由子类决定创建哪个具体类型的场景。

- 抽象工厂 适用于需要一“族”相关对象且它们必须彼此匹配的场景(例如:某服务商专属的客户端、映射器和验证器)。

- 原型模式 适用于克隆一个已配置好的对象比重新构建它更便宜或更安全时,尤其当初始化代价昂贵时。

分支二:组织结构(Structural patterns)
当代码逻辑上是对的,但因为边界不清导致使用别扭时使用此分支:外部接口渗入应用逻辑、子系统需要过多步骤才能安全使用,或组合关系难以管理。
第一步:你是否在桥接不兼容的接口?
当你的内部代码期望一种接口,而外部依赖提供的是另一种,适配器模式是直截了当的解法。它能保护你的领域免受厂商特定的结构与命名影响。
# 你的应用期望:
payment_processor.process(amount, card)
# 服务商提供:
provider.execute_payment(card_info, transaction_amount)
class ProviderAdapter
def initialize(provider)
@provider = provider
end
def process(amount, card)
@provider.execute_payment(card.to_provider_format, amount)
end
end
一个实用的准则:让适配器专注在“翻译”。当一个适配器开始包含业务规则时,把这些规则拆到单独组件里,以保持边界干净。

第二步:某个子系统是否复杂到不易正确使用?
如果一个库或内部子系统有多步操作,且必须按正确顺序调用,引入一个外观模式。一个好的外观让“安全路径”变得容易,也降低工程师误用底层组件的概率。

例子:“视频转换”工作流可能涉及探测、转码、元数据提取、存储上传与清理。一个外观可以只暴露单一入口,同时让内部编排自在演进。
第三步:你是否需要可选功能又不想出现“子类爆炸”?
当你需要如日志、加密、压缩、缓存这样的组合时,纯粹通过继承会带来组合数量的爆炸。装饰器模式让组合保持局部且显式。

当每个包装器小而可预测时,装饰器效果最佳。如果各个包装器相互依赖,就很难推断调用顺序与副作用。
第四步:你是否需要一个“替身对象”?
当你希望在一个看起来本地的接口后面实现懒加载、缓存、访问控制、埋点或远程调用时,使用代理模式。

第五步:你是否有树形结构并希望统一处理?
当你的领域天然形成层级,且你希望对叶子节点与容器一视同仁时使用组合模式(文件系统是经典示例;UI 组件与嵌套内容结构也很常见)。

第六步:你是否在为重复的共享状态支付内存成本?
当大量对象需要共享相同数据,且复制这些数据的成本很高时,Flyweight(享元)模式就变得有意义。它在典型的Web应用代码中较少见,但在编辑器、渲染引擎或大型内存模型等场景中值得考虑。

第七步:你是否希望独立地变化抽象与实现?
当系统存在两个独立的变化维度(例如:“导出格式”与“导出目的地”,或“设备类型”与“控制类型”),并且希望避免因组合而导致子类数量爆炸时,可以使用Bridge(桥接)模式。

分支三:处理行为(Behavioural patterns)
当主要问题在于规则与流程逻辑频繁变化时,应考虑行为型模式。典型信号包括:某个方法积累了大量的if-else分支、某个算法需要根据不同客户或套餐而变化、或者某条处理流水线难以整洁地扩展。
第一步:请求是否要流经一串相互独立的步骤?
Chain of Responsibility(责任链)模式适用于构建中间件式的处理管线,其中每个步骤(处理器)可以独立决定是处理请求还是将其传递给链中的下一个处理器。
class Handler
def initialize(next_handler = nil)
@next = next_handler
end
def call(request)
return unless handle?(request)
@next&.call(request)
end
end
当每个处理器职责单一,且“停止处理”与“继续传递”的契约清晰时,该模式运行良好。如果处理器之间不可预测地修改共享状态或产生内部依赖,模式就会变得难以维护。

第二步:你是否需要排队动作、记录日志、重试或撤销?
当将操作封装为对象能带来运营层面的好处时(例如:支持作业队列、重试机制、审计日志、“稍后执行”的工作流或撤销/重做功能),Command(命令)模式是合适的选择。

第三步:你是否需要在不改调用方的前提下切换算法?
当存在多个行为相似但实现不同的算法,且希望调用方代码保持稳定时,Strategy(策略)模式是投资回报率很高的方案。它常见于支付服务商选择、路由决策、推荐策略、限流算法与数据格式化等场景。

一个明显的信号是代码中反复出现条件分支(例如:“如果套餐是X则执行A,否则执行Y”),以及为这些分支重复搭建的测试。
第四步:行为是否由状态驱动,而条件分支不断倍增?
当对象的行为由其内部状态决定,并且管理状态迁移的条件分支逻辑变得复杂时,State(状态)模式非常有用。它通过将每个状态下的行为封装到独立的类中,来简化复杂的条件判断。

第五步:你是否需要一对多的通知?
Observer(观察者)模式为订阅式更新提供了清晰的机制,尤其适合配合领域事件使用。但需注意,它可能隐藏控制流。应保持观察者的可见性,并避免引入难以追踪的副作用。

第六步:你是否需要快照与恢复?
Memento(备忘录)模式适用于需要实现撤销、回滚或恢复到先前版本的功能,同时无需暴露对象的内部表示细节。

第七步:你是否需要一个协调者,让对象不要彼此直接依赖?
在复杂的协同场景中(尤其是UI组件交互或工作流编排),Mediator(中介者)模式可以降低对象间的直接耦合。风险在于可能将过多逻辑集中到中介者中。保持中介者职责的狭窄和清晰,有助于其管理。

第八步:你是否需要在稳定的对象结构上添加新操作?
当对象结构稳定(例如抽象语法树AST),且需要在不修改结构类的前提下为其添加新的操作时,Visitor(访问者)模式最为有用。在常规应用业务代码中,它不如Strategy或Chain常见,但在编译器、解释器等特定领域价值显著。

完整的决策树

把决策树应用到常见场景
1)通知发送(Email、SMS、Push)
当发送规则增多、渠道扩展时,容易陷入使用条件分支的默认方案。此时,Strategy模式更为合适,因为它允许在不修改调用方代码的情况下灵活添加新的通知渠道。
一个实用的实现是定义一个接口(如NotificationChannel#send(user, message)),为每个渠道提供具体实现,并通过一个选择器(基于配置或特性开关)来动态选择对应的策略。
2)API请求处理(限流 → 认证 → 业务处理)
当API请求需要按照既定顺序通过一系列检查或处理步骤时,Chain of Responsibility模式能让每一步都保持小巧且可独立测试。同时,调整步骤顺序或插入新步骤也会更加安全,因为处理链的契约是显式定义的。
3)报告生成(选项众多、格式多样)
当报告配置涉及多个参数且“有效组合”的逻辑复杂时,Builder(建造者)模式有助于构造过程。当需要在不同输出格式(如PDF/CSV/XLSX)之间切换,而又不想将格式化逻辑埋藏在条件分支中时,Strategy模式是更好的选择。
关注“鲸栖”小程序,掌握最新AI资讯
本文来自网络搜集,不代表鲸林向海立场,如有侵权,联系删除。转载请注明出处:https://www.itsolotime.com/archives/22151
