设计模式核心:从创建型到行为型
设计模式核心:从创建型到行为型
本文不罗列 23 个模式的八股定义,而是讲清它们各自解决什么问题、什么时候不该用,以及在现代语言里哪些模式已经被语法本身取代。
先分清:模式解决的是"变化"
设计模式的本质是把"会变化的部分"和"稳定的部分"分开。判断一个模式该不该用,只看一件事:这里有没有需要独立变化、且未来可能扩展的维度。没有变化点硬套模式,只会增加一层无谓的间接。
SOLID 是这套思想的原则版:
- 单一职责:一个类只有一个变化原因。
- 开闭:对扩展开放,对修改关闭。
- 里氏替换:子类能替换父类而不破坏行为。
- 接口隔离:接口要小而专。
- 依赖倒置:依赖抽象,不依赖实现。
创建型:怎么把对象造出来
工厂方法
当创建逻辑复杂、或需要按类型返回不同实现时,把 new 收进工厂。
type Notifier interface{ Send(msg string) error }
func NewNotifier(kind string) Notifier {
switch kind {
case "sms":
return &SMSNotifier{}
case "email":
return &EmailNotifier{}
default:
return &NoopNotifier{}
}
}
适用:调用方只关心接口,不关心具体类型。过度使用会让简单 new 变成一堆工厂类。
建造者
对象参数多、可选参数多、构造步骤有顺序时,用链式建造者避免"构造函数参数爆炸"。
Order order = Order.builder()
.userId("u1")
.items(items)
.coupon("C10")
.build();
Go 里常用函数式选项(functional options)达到同样效果。
单例
全局只允许一个实例。问题在于它引入了全局状态和隐式依赖,测试困难、并发初始化要加锁。现代做法通常是依赖注入容器管理生命周期,而不是到处 getInstance()。
结构型:怎么把对象组合起来
适配器
把不兼容的接口转成目标接口。典型场景是接入第三方 SDK、兼容旧接口。
type LegacyLogger struct{}
func (l *LegacyLogger) WriteLog(s string) { /* ... */ }
type Logger interface{ Info(msg string) }
type LoggerAdapter struct{ legacy *LegacyLogger }
func (a *LoggerAdapter) Info(msg string) { a.legacy.WriteLog("[INFO] " + msg) }
装饰器
在不修改原对象的前提下动态叠加能力,比如给 HTTP handler 加日志、鉴权、限流。Go 的中间件、Python 的 @decorator 都是这个思路。
func WithLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println(r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
代理
在真实对象前加一层,控制访问:延迟加载、权限校验、远程调用、缓存。和装饰器的区别是目的不同——装饰器加功能,代理控访问。
组合
把树形结构统一对待,叶子节点和容器节点实现同一接口。文件系统、组织架构、UI 组件树都是典型。
行为型:怎么分配职责与通信
策略
把一组可互换的算法封装起来,运行时选择。替代冗长的 if/else 或 switch。
type Pricing interface{ Calc(amount float64) float64 }
type NormalPricing struct{}
func (NormalPricing) Calc(a float64) float64 { return a }
type VIPPricing struct{}
func (VIPPricing) Calc(a float64) float64 { return a * 0.8 }
func Price(p Pricing, amount float64) float64 { return p.Calc(amount) }
观察者
一对多依赖,状态变化时通知所有订阅者。事件总线、消息推送、前端响应式系统都是它。注意控制通知顺序和异常隔离,否则一个订阅者报错会拖垮整条链路。
责任链
请求沿链传递,直到被处理。中间件链、审批流、日志分级处理都适用。要防止链过长导致排查困难,并明确"无人处理"时的兜底。
模板方法
父类定义流程骨架,子类实现可变步骤。适合流程固定、细节可变的场景,比如数据导入导出、审批流程。缺点是继承耦合强,现代更倾向用组合 + 策略替代。
哪些模式正在被语言特性取代
| 传统模式 | 现代替代 |
|---|---|
| 策略 / 命令 | 一等函数、闭包、lambda |
| 工厂 | 依赖注入容器、泛型 |
| 单例 | 容器管理的单例作用域 |
| 迭代器 | 语言内置 for-range / iterator |
| 观察者 | 响应式库、事件总线、Channel |
这不是说模式没用了,而是不要为了"用模式"而写模式。语言已经表达清楚的地方,再加一层抽象是负担。
反模式:比不用模式更糟
- 过度抽象:为不存在的扩展点设计接口,读代码要跳五层。
- 万能基类:所有类继承一个巨型 Base,改一处影响全站。
- 隐式全局状态:单例满天飞,测试互相污染。
- 模式堆砌:一个类同时是工厂、策略、观察者,谁也看不懂。
- 为了设计模式而设计模式:面试背得熟,线上没人维护。
实战:怎么判断该不该上模式
- 先写最直白的实现,能跑通再谈抽象。
- 出现第二处相似变化时,再抽接口(三次法则)。
- 抽象后如果调用方更难看懂,说明抽象错了。
- 优先组合而非继承,优先函数而非类。
- 模式名只用于沟通,不写进类名(
XxxFactory泛滥是坏味道)。
总结
| 类别 | 代表模式 | 解决的问题 |
|---|---|---|
| 创建型 | 工厂、建造者、单例 | 对象创建复杂或需受控 |
| 结构型 | 适配器、装饰器、代理、组合 | 对象组合与接口适配 |
| 行为型 | 策略、观察者、责任链、模板方法 | 职责分配与通信 |
设计模式是"变化点的工具箱",不是代码模板。真正重要的是识别哪里会变、把变化隔离开,至于是不是叫某个模式名,并不重要。
- 点赞
- 收藏
- 关注作者
评论(0)