浅谈

在大学时代的课程中就有一门课叫设计模式,当初觉得非常高大上。老师说这门课是研究生课程,更加让人着迷。但作为系主任的他在选课时觉得有必要学这门课,现在看了主任还是颇具眼光。这也奠定了自己在开发代码前都会考虑项目的长远利益。

设计模式是一种代码编写范式,不同业务场景采用不同的设计范式,能让整体具备可维护性。你的同事也会因为你在项目中的设计思维而夸赞你,当然前提你们都受过设计模式的熏陶。否则,得通过小组学习共享或 code review 等形式统一认识,减少认知负担。

程序员的品位很大一部分体现在他们的设计模式中。下面章节建立在你已单独了解每个设计模式的使用,所以不会赘述过多实现细节,聚焦于核心区别和使用场景等。

设计模式是软件设计中解决常见问题的可复用方案。它们通过提供结构化的最佳实践,帮助开发者提高代码的可维护性、灵活性和可扩展性。以下是设计模式的不同之处、设计意图、分类及比较的详细总结:

核心分类

设计模式通常分为三大类:创建型模式(Creational Patterns)、结构型模式(Structural Patterns、行为型模式(Behavioral Patterns)。

  • 创建型模式是对象创建的工具箱,解决实例化过程的复杂性。
  • 结构型模式积木搭建指南,优化对象组合与接口设计。
  • 行为型模式协作说明书,定义对象间的高效交互方式。

创建型模式

意图:控制对象的创建过程,解耦对象的创建与使用。
常见模式

  • 单例(Singleton):确保一个类仅有一个实例,并提供全局访问点。
  • 工厂方法(Factory Method):将对象创建延迟到子类,由子类决定实例化哪个类。
  • 抽象工厂(Abstract Factory):创建一组相关或依赖对象的家族,而不指定具体类。
  • 建造者(Builder):分步构造复杂对象,允许灵活配置。
  • 原型(Prototype):通过克隆已有对象来创建新对象,避免重复初始化。

适用场景:需要灵活控制对象创建逻辑(如依赖注入、配置化创建)。

单例

核心功能:创建全局可共享的单一实例。

单例模式应该是所有模式最简单的。它的核心作用就是创建一个在整个程序运行期间独立存在的单一实例,全局范围所共享。

所以你看到了 xxSingleton 命名的 class 就知道其作用全局范围。

实现的相关:

  • 在单线程 JavaScript 环境无需考虑线程同步,但在 Java 这样的多线程语言就需要考虑单例实现时的线程安全
  • 单例的创建可以在使用的时候实例化,即延迟初始化 (lazy instantiation)。因此通常使用静态方法动态实例化。例如:XXSingleton.getInstance()。
工厂

核心功能:统筹创建多个具有相同类型的实例。

工厂模式有:工厂方法(Factory Method)和抽象工厂(Abstract Factory)。

工厂方法用于创建单一类别的类。而抽象工厂创建多个类别的类,不同类别的类都有自己独立的工厂实现,且每个工厂需要实现多个统一接口。

建造者

核心功能:解决构造函数参数过多、对象构造逻辑复杂的问题。

也就是为每个配置项提供一个设置函数。通常提供默认配置,用户可以根据自己的需求改变配置。

应用场景

  1. 售卖电脑选择不同:CPU、内存、显卡等;
  2. 售卖奶茶选择不同:几分糖、大小、添加料、温度选择等。

结构型模式

意图:通过组合类和对象形成更大的结构,解决对象之间的协作问题。
常见模式

  • 适配器(Adapter):转换接口,使不兼容的类可以协同工作。
  • 装饰器(Decorator):动态地为对象添加职责,避免继承导致的类爆炸。
  • 代理(Proxy):为其他对象提供代理以控制访问(如延迟加载、权限控制)。
  • 组合(Composite):以树形结构组合对象,统一对待单个对象和组合对象。
  • 外观(Facade):为复杂子系统提供统一的高层接口。
  • 桥接(Bridge):分离抽象与实现,使两者可以独立变化。
  • 享元(Flyweight):共享细粒度对象,减少内存占用。

适用场景:需要简化系统结构或优化对象间交互(如接口适配、功能扩展)。

适配器

核心功能:解决接口不兼容问题。

装饰器

核心功能:不修改原有对象的基础上,增加功能。

装饰器在 Typescript 里有实现,直接就叫装饰器。而 Java 中的注解也是装饰器的一种实现。

关键性质

在开发中,装饰器看来是一种语法糖,但他们通常有一条重要的性质:装饰器对被装饰的对象不做任何改变(装饰器和被装饰者通过组合形式结合一起),也就是设计原则中的复用中的组合优先原则

在 React 中,曾经的高阶组件就是装饰器的一种实现,不过由于 hooks 的出现,不再流行。

一个最简单的装饰器实现如下:

function decorator(obj) {
  return obj;
}

由于不改变被装饰者,因此多个装饰器可以作用于一个被装饰者,而不会对原有对象产生影响。

代理

核心功能:用于控制行为。也就是说通常不改变被代理对象的行为,而是决定行为要不要执行。

行为型模式

意图:管理对象之间的交互和职责分配,提高通信效率。
常见模式

  • 策略(Strategy):定义算法族,使其可互换,独立于客户端。
  • 观察者(Observer):定义对象间的一对多依赖,状态变化时自动通知。
  • 命令(Command):将请求封装为对象,支持撤销、队列和日志。
  • 职责链(Chain of Responsibility):将请求沿处理链传递,直到被处理。
  • 状态(State):允许对象在其内部状态改变时改变行为。
  • 模板方法(Template Method):定义算法骨架,子类重写某些步骤。
  • 迭代器(Iterator):提供顺序访问聚合对象元素的方法,无需暴露内部结构。

适用场景:需要解耦对象间的通信(如事件处理、流程控制)。

观察者

核心功能:实现对注册者的广播功能。

主题需要观察者对其进行订阅,主题接收到事件后,对每一位观察者发送事件信息。

发布订阅模式 (Publish-Subscribe Pattern)

相比观察者模式直接注册主题,发布订阅模式引入中间的事件通道(Event Channel),解耦更彻底,观察者与主题不直接通信。

应用场景:

  1. 前端框架
    • React:状态管理库(如 MobX)基于观察者模式实现响应式数据更新。
    • Vue:通过 Object.definePropertyProxy 监听数据变化,触发组件重新渲染。
  2. 后端开发
    • Spring FrameworkApplicationEventApplicationListener 实现事件监听。
    • 消息队列:如 Kafka 生产者-消费者模型,本质是分布式观察者模式
  3. 系统设计
    • 实时聊天:用户发送消息后,服务器通知所有在线客户端。
    • 微服务架构:服务间通过事件总线(如 RabbitMQ)同步状态变化。
  • 经典实现:Java 的 PropertyChangeListener、C# 的 event 关键字、RxJS 的 Observable
迭代器

核心功能:提供顺序访问聚合对象元素的方法,无需暴露内部结构。

迭代器更多表现为一个编程概念。它约定一种接口格式,让对象实现该接口方法提供外部访问。它必然有个 next() 方法用于访问聚合对象内的每个元素。

// 迭代器接口
interface Iterator<T> {
    boolean hasNext();
    T next(); // 必有该方法
}

你几乎在任何使用迭代器技术的编程语言中,发现其迭代器接口都存在 next() 方法,不过在访问方式上有所区别。

在 JavaScript 中,StringArrayTypedArrayMapSet 都实现了 [Symbol.iterator]() 迭代器接口。

  1. 通过 for...of 语法访问:
const myArray = ["a", "b", "c"];

for (const element of myArray) {
  console.log(element);
}
  1. 通过内部迭代器访问:
const myArray = ["a", "b", "c"];
const ite = myArray[Symbol.iterator]();
ite.next()
// { value: 'a', done: false }
ite.next()
// { value: 'b', done: false }

JavaScript 是一种拥有非常多语法糖的语言,除此以上之外还有展开语法yield*解构语法等方式访问。

在 Java 中, ListSetQueue 都实现了 java.util.Iterator 接口,也有不同访问方式。

  1. foreach 语法:
for (String element : list) {
    System.out.println(element);
}
  1. 通过内部迭代器访问:
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));

// 获取迭代器
Iterator<String> iterator = list.iterator();

// 遍历
while (iterator.hasNext()) {
    String element = iterator.next();
    System.out.println(element);
}

// Java 8+ 的 `forEachRemaining()`
Iterator<String> iterator1 = list.iterator();
iterator1.forEachRemaining(element -> System.out.println(element));

核心差异

关注点不同

  • 创建型:关注“如何创建对象”。
  • 结构型:关注“如何组织类和对象”。
  • 行为型:关注“对象如何协作与分配职责”。

典型对比

模式对比核心区别
工厂方法 vs 抽象工厂工厂方法针对单一产品,抽象工厂针对产品族。
适配器 vs 桥接适配器解决接口不兼容,桥接分离抽象与实现。
装饰器 vs 代理装饰器动态添加功能,代理控制访问(可能不修改行为)。
策略 vs 状态策略是主动切换算法,状态是被动响应内部状态变化。
观察者 vs 中介者观察者直接通信,中介者通过中间对象协调。

如何选择设计模式?

  1. 明确问题类型:先确定问题是创建、结构还是行为相关。
  2. 解耦需求:若需解耦对象创建(如工厂)、功能扩展(如装饰器)或通信(如观察者)。
  3. 灵活性要求:策略模式支持运行时切换算法,状态模式根据状态动态调整行为。
  4. 可维护性:避免过度设计,选择复杂度与需求匹配的模式。

实际应用中,模式常结合使用(如工厂方法+单例,装饰器+组合)。关键在于理解问题本质,而非机械套用模式。