设计模式入门:构建优雅且可维护的代码


在软件工程中,设计模式(Design Patterns) 是针对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。

设计模式并不是一段可以直接复制的代码,而是一套解决问题的思路和模板。通过学习设计模式,我们可以写出更高内聚、低耦合、易于扩展的代码。


1. 为什么要学设计模式?

  1. 重用成功的代码结构:不需要每次都重新发明轮子。
  2. 团队沟通的“暗号”:如果你跟同事说“这里用了观察者模式”,大家立刻就能明白系统的结构,大大降低沟通成本。
  3. 遵循设计原则:模式往往是 SOLID 等设计原则的具体实践。

2. 设计模式的三大分类

1994 年,著名的“四人帮”(GoF, Gang of Four)在《设计模式》一书中将模式分为三大类:

A. 创建型模式 (Creational Patterns)

关注对象的创建过程,旨在将“对象的创建”与“对象的使用”解耦,使系统在创建对象时更加灵活。

  • 单例模式 (Singleton):确保一个类在全局范围内只有一个实例,并提供一个全局访问点。
    • 场景:数据库连接池、日志记录器、全局状态管理(如 Redux)。
  • 工厂方法模式 (Factory Method):定义一个创建对象的接口,但由子类决定实例化哪一个类。这使得一个类的实例化延迟到其子类。
    • 场景:UI 框架根据不同平台(iOS/Android)创建不同的按钮组件。
  • 抽象工厂模式 (Abstract Factory):提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
    • 场景:一套跨平台的皮肤系统,包含按钮、输入框、滚动条等一整套 UI 组件。
  • 建造者模式 (Builder):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
    • 场景:配置复杂的编辑器选项,或者构建包含大量可选字段的数据对象(如 SQL 查询构建器)。
  • 原型模式 (Prototype):用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
    • 场景:创建一个开销很大的对象,后续通过克隆已有的实例来提高效率。

B. 结构型模式 (Structural Patterns)

关注类或对象的组合,通过组合简单的类或对象来形成更庞大的结构,以满足更复杂的需求。

  • 适配器模式 (Adapter):将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
    • 场景:在现代系统中集成旧版的第三方 SDK。
  • 代理模式 (Proxy):为其他对象提供一种代理以控制对这个对象的访问。
    • 场景:图片的延迟加载、权限校验、前端框架中的双向绑定数据拦截。
  • 装饰器模式 (Decorator):动态地给一个对象添加一些额外的职责。相比生成子类,装饰器更加灵活且不会导致类膨胀。
    • 场景:给基础的网络请求方法添加缓存逻辑、重试机制或日志记录。
  • 外观模式 (Facade):为子系统中的一组接口提供一个一致的界面,Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
    • 场景:封装一个库的底层复杂逻辑,对外只暴露几个简单的 API。
  • 组合模式 (Composite):将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
    • 场景:文件系统、公司组织架构图、DOM 树结构。

C. 行为型模式 (Behavioral Patterns)

关注对象之间的通信,描述对象之间如何相互作用、如何分配职责,是设计模式中最为庞大的一类。

  • 策略模式 (Strategy):定义一系列算法,并将每一个算法封装起来,使它们可以相互替换。算法的变化不会影响到使用算法的客户。
    • 场景:表单验证规则(根据不同规则进行校验)、电商平台的折扣计算(满减、打折、返现)。
  • 观察者模式 (Observer):定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
    • 场景:事件监听、响应式数据(Vue/React)、广播消息。
  • 模板方法模式 (Template Method):在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。使得子类可以在不改变算法结构的情况下,重新定义算法中的某些特定步骤。
    • 场景:定义爬虫抓取的标准流程,但具体的页面解析逻辑交给不同的子类实现。
  • 命令模式 (Command):将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化。
    • 场景:编辑器的撤销/重做功能、任务队列管理。
  • 状态模式 (State):允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
    • 场景:订单状态流转(待支付 -> 已支付 -> 待发货)、游戏中角色的不同状态(行走、攻击、防御)。

3. 核心模式深度解析

3.1 单例模式 (Singleton)

应用场景:全局状态管理(如 Vuex/Pinia)、数据库连接池、日志记录器。

class Database {
  private static instance: Database;
  
  private constructor() {} // 私有构造函数,防止外部 new

  public static getInstance(): Database {
    if (!Database.instance) {
      Database.instance = new Database();
    }
    return Database.instance;
  }
}

const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true

3.2 观察者模式 (Observer)

应用场景:UI 组件库、自定义事件系统。

// 主题(被观察者)
class Subject {
  private observers: Function[] = [];

  subscribe(fn: Function) {
    this.observers.push(fn);
  }

  notify(data: any) {
    this.observers.forEach(fn => fn(data));
  }
}

const news = new Subject();
news.subscribe((data) => console.log("观察者 A 收到更新:", data));
news.subscribe((data) => console.log("观察者 B 收到更新:", data));

news.notify("新博文发布了!");

4. 绕不过去的 SOLID 原则

设计模式的灵魂在于遵循 SOLID 原则

  1. S (单一职责原则):一个类只负责一件事。
  2. O (开闭原则):对扩展开放,对修改关闭。
  3. L (里氏替换原则):子类必须能替换掉它们的父类。
  4. I (接口隔离原则):不应该强迫客户依赖于它们不使用的方法。
  5. D (依赖倒置原则):高层模块不应该依赖底层模块,两者都应该依赖抽象。

5. 什么时候不该用设计模式?

过度设计(Over-engineering) 是新手的常见陷阱。

  • 如果你的项目规模很小,逻辑很简单,生搬硬套设计模式只会增加代码的复杂度。
  • KISS 原则 (Keep It Simple, Stupid):保持简单。只有当代码变得难以维护或频繁变动时,才是引入设计模式的最佳时机。

总结

设计模式是开发者的武功秘籍。理解它们并不难,难的是在合适的场景下“无招胜有招”。先从最常用的单例、观察者、策略模式开始练习,你会发现自己看待代码的眼光会有质的飞跃。