设计模式起源于建筑行业。由 GoF 四人引入软件工程。提出和总结了对于一些常见软件设计问题的标准解决方案,称为软件设计模式。但是近来能在看到设计模式不乏批评的声音(比如 “一股 Java 味”)。
这有许多因素,一方面,当提起设计模式时,往往就会被关联到 GoF 书中所指的设计模式。而那本书是基于 90 年代的 C++ 和 Java。是 OOP 的,而且语言本身的表达能力,没有一等公民函数,缺乏具名参数,闭包。在这种情况下,为了解决代码的组织问题,就不得不 “发明” 一些抽象来补足语言表达能力的缺失。另外一方面,教条主义盛行,设计模式被过度使用,没必要使用的场景也过度使用,反而带来了额外的抽象成本。
关注设计模式解决的问题#
设计模式来自工程实践,它处理的是现实工程中的复杂度。这里的复杂度可能是业务本身的复杂度,也可能是框架、语言表达里欠缺的复杂度。
因此,设计模式的具体形态会强烈受到语言、框架影响。同一个问题,在不同场景中可能有完全不同的表达方式。因此,我们更应当关注设计模式解决的问题,而非如何实现 XX 设计模式。
如果语言缺少高阶函数,那么实现动态可替换行为就需要通过接口和类表达,如果语言缺少命名参数和默认参数,那么复杂对象构造就容易发展出 Builder。如果语言缺少模式匹配和代数数据类型,那么某些类型分派问题就可能通过 Visitor 解决。
例如要解决一段逻辑中的 “可替换行为”:
- 在 Java 中,可能表现为 Strategy 接口和多个实现类。
- 在 Kotlin/JS/Go 中,可能只是一个函数参数。
- 在 Rust 中,可能表现为 trait、enum 或 pattern matching。
设计模式可以理解为用已有语言机制,对更高层抽象能力的模拟。
而随着编程语言自身的研究发展,一些模式会变成语言特性或标准库能力。
例如 Kotlin 中:
- Singleton →
object - Lazy Initialization →
lazy - Strategy → lambda / function type
- Builder → 命名参数、默认参数、DSL
- Delegation →
by - State / Visitor 的一部分场景 → sealed class +
when - Observer 的一部分场景 → Flow / StateFlow / SharedFlow
这些问题已经变成了语言特性,也就不再需要动手实现这些设计模式了。
而另外一部分不适合内化到语言特性的设计模式,则变成了框架的一部分,业务开发则不需要面对类似的问题,而是使用框架暴露出的更高层抽象。
- 依赖注入:Factory、Singleton、Service Locator。
- Web 中间件:Chain of Responsibility、Decorator、Proxy
- ORM: Repository、Data Mapper、Unit of Work
- UI 框架: Observer、State、Composite;
- RPC:Proxy、Adapter、Facade
工程问题#
资源的生命周期管理#
对象、资源、服务应该什么时候创建,如何创建,由谁持有?什么时候释放?是否需要复用?是否需要全局唯一?
而每个对象资源,又会有因场景不同而有不同的思路。
比如,
- 数据库连接,可能需要在启动时创建,关闭时释放。
- HTTP client
- 配置对象,可能
- 缓存实例
- 文件句柄
- 线程池
- 应用服务生命周期
边界隔离#
如何隔离不稳定、不兼容、不可信或外部的系统边界?
- 第三方 API;
- 数据库访问;
- 微服务接口;
- SDK 封装;
- 新旧系统迁移;
- 外部支付、登录、通知服务;
- 跨平台能力适配。
数据流与通知#
状态变化如何传播?事件如何分发?异步数据如何组织?
- 前端状态管理;
- 后端事件驱动;
- 实时协作;
- 消息队列;
- 响应式编程;
- 工作流系统
这类模式经常被框架或 runtime 吸收,但问题本身仍然非常核心。
组合与扩展#
如何在不修改原有代码的情况下增加能力?如何让功能可以组合、插拔和复用?
- Web middleware;
- 请求拦截器;
- 编辑器插件;
- 编译器插件;
- Agent tools;
- 日志、鉴权、缓存、监控;
- 可插拔业务规则。