设计模式

1.1K
0
0
最后修改于

设计模式起源于建筑行业。由 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;
  • 日志、鉴权、缓存、监控;
  • 可插拔业务规则。