深入解析五大创建型设计模式:从工厂到单例的工程实践
1. 从“new”说起为什么我们需要五种创建模式如果你写过面向对象编程的代码那么new这个关键字对你来说一定不陌生。在 Java、C#、JavaScript 等语言里我们早已习惯了let obj new MyClass()这样的操作。它看起来简单直接就像从模具里压出一个标准零件。但当你真正深入项目尤其是需要构建复杂、灵活、可复用的系统时你可能会发现仅仅依赖new来创建对象就像试图用一把锤子解决所有问题——有时能行但更多时候会让你陷入僵局。举个例子假设你正在开发一个图形编辑器需要创建圆形、矩形、三角形等不同形状。最直接的想法可能是为每个形状定义一个类然后在需要的地方new Circle()、new Rectangle()。这没问题直到你的编辑器需要支持用户从文件加载形状或者从网络接收序列化的形状数据。此时你拿到的可能只是一个字符串circle或一个包含类型信息的 JSON 对象。你无法直接new “circle”()。这就是第一个痛点创建逻辑与使用逻辑的强耦合。客户端代码必须明确知道要创建哪个具体类这降低了代码的灵活性。再比如创建一个数据库连接对象。这个过程可能涉及加载驱动、解析配置、建立网络连接、身份验证等一系列复杂且耗时的操作。如果你在每次需要连接时都new Connection()不仅性能低下还可能因为连接数过多导致数据库压力过大。你真正需要的可能是一个“连接池”它管理着一组预先创建好的或可复用的连接对象。这就是第二个痛点复杂对象的创建过程需要封装和优化。“对象的创建模式”要解决的正是这些痛点。它们不是语法规定而是经过大量实践总结出来的最佳实践套路用于将对象的创建过程抽象、封装、优化从而使系统更灵活、更可维护、更高效。今天要讨论的这五种模式——工厂方法、抽象工厂、建造者、原型、单例——就是其中最经典、应用最广泛的核心。它们回答了不同场景下的核心问题如何让创建过程更灵活如何创建一系列相关对象如何一步步构建一个复杂对象如何高效地克隆对象如何确保一个类只有一个实例理解它们你就能在代码中“看见”那些隐形的设计意图而不仅仅是实现功能。下面我们就逐一拆解看看这五种模式如何将我们从简单的new中解放出来。2. 工厂方法模式将具体类型的选择权延迟工厂方法模式可能是你最早接触也最直观的一种创建模式。它的核心思想非常简单定义一个用于创建对象的接口但让子类决定实例化哪一个类。换句话说它把new这个具体动作从客户端代码转移到了一个专门的“工厂”方法里。2.1 模式结构与经典实现我们用一个日志记录器的例子来说明。假设你的系统需要支持将日志记录到文件、数据库或控制台。最原始的写法可能是这样// 客户端代码 const type getConfig(‘log_type’); // 假设从配置读取 ‘file‘ if (type ‘file‘) { logger new FileLogger(); } else if (type ‘database‘) { logger new DatabaseLogger(); } else { logger new ConsoleLogger(); } logger.log(“Something happened.”);这段代码的问题很明显客户端使用日志的代码直接依赖了所有具体的日志类FileLogger,DatabaseLogger。如果新增一个网络日志你需要修改所有使用日志的客户端代码。这违反了“开闭原则”对扩展开放对修改关闭。工厂方法模式如何改造它首先我们定义一个所有日志记录器都要实现的接口或抽象类// 产品接口 class Logger { log(message) { throw new Error(‘Method “log()” must be implemented.’); } } // 具体产品 class FileLogger extends Logger { log(message) { console.log(Writing “${message}” to file.); // 实际的文件写入逻辑... } } class DatabaseLogger extends Logger { log(message) { console.log(Storing “${message}” to database.); // 实际的数据库操作逻辑... } }接下来是关键我们创建一个“创建者”类它声明了工厂方法。// 创建者Creator抽象类 class LoggerFactory { // 这就是工厂方法 createLogger() { throw new Error(‘Method “createLogger()” must be implemented.’); } // 一个可能用到产品的操作 someOperation() { const logger this.createLogger(); // 调用工厂方法 logger.log(“Factory method is working.”); } } // 具体创建者 class FileLoggerFactory extends LoggerFactory { createLogger() { // 在这里决定创建哪个具体产品 return new FileLogger(); } } class DatabaseLoggerFactory extends LoggerFactory { createLogger() { return new DatabaseLogger(); } }现在客户端代码的使用方式变了// 客户端代码 let factory; const configType getConfig(‘log_type’); if (configType ‘file‘) { factory new FileLoggerFactory(); } else if (configType ‘database‘) { factory new DatabaseLoggerFactory(); } const logger factory.createLogger(); logger.log(“Application started.”); // 或者直接使用创建者提供的操作 factory.someOperation();看起来变化不大但依赖关系发生了根本性转移。客户端现在只依赖LoggerFactory抽象和Logger抽象。具体的FileLogger和DatabaseLogger类只在对应的具体工厂FileLoggerFactory中被new出来。新增一种日志类型时你只需要新增一个具体产品类和一个对应的具体工厂类而无需修改任何现有的客户端代码。客户端通过配置来决定使用哪个工厂创建逻辑被完美地隔离和封装了。实操心得何时该用工厂方法当你无法预知代码运行时需要创建的具体类型或者希望将创建逻辑与业务逻辑解耦时工厂方法是首选。它在框架设计中极为常见比如 Spring 框架中的BeanFactory客户端通过接口获取 Bean完全不用关心这个 Bean 是单例还是原型是本地类还是远程代理。记住它的核心是“延迟决策”将具体类的实例化推迟到子类。2.2 简单工厂的变体与权衡你可能会听到“简单工厂”这个概念。它并不是 GoF 23种设计模式之一而是一种编程习惯。它通常是一个单独的类里面有一个静态方法根据参数返回不同的产品对象。class SimpleLoggerFactory { static createLogger(type) { switch (type) { case ‘file‘: return new FileLogger(); case ‘database‘: return new DatabaseLogger(); default: return new ConsoleLogger(); } } } // 使用 const logger SimpleLoggerFactory.createLogger(‘file’);简单工厂的优点很明显代码更简洁无需创建一堆工厂子类。但它有一个致命缺点违反了开闭原则。当你需要新增一个类型时你必须修改SimpleLoggerFactory中的switch或if-else代码块。在大型、稳定的框架中这可能是不可接受的。因此简单工厂更适合于创建逻辑稳定、类型不多的场景或者作为向标准工厂方法模式演进的一个中间步骤。3. 抽象工厂模式创建“产品家族”的契约工厂方法模式关注的是单个产品的创建。但如果我们需要创建一系列相互关联或依赖的产品对象呢比如开发一个 UI 库需要为 Windows 风格创建一套按钮、文本框、对话框为 Mac 风格创建另一套风格完全不同的同类控件。你不可能让一个 Windows 按钮和一个 Mac 文本框混用它们必须成套出现。这就是抽象工厂模式大显身手的地方。抽象工厂模式提供了一个接口用于创建一系列相关或依赖的对象而无需指定它们具体的类。3.1 模式解读与多平台 UI 案例我们继续用 UI 套件的例子。首先定义抽象产品接口按钮和文本框。// 抽象产品按钮 class Button { render() {} onClick(f) {} } // 抽象产品文本框 class TextBox { render() {} getValue() {} }然后为不同操作系统创建具体产品// 具体产品Windows 风格按钮 class WindowsButton extends Button { render() { console.log(“Rendering a button in Windows style.”); } onClick(f) { console.log(“Windows button clicked.”); f(); } } // 具体产品Mac 风格按钮 class MacButton extends Button { render() { console.log(“Rendering a button in macOS style.”); } onClick(f) { console.log(“Mac button clicked.”); f(); } } // 类似地定义 WindowsTextBox 和 MacTextBox... class WindowsTextBox extends TextBox { /* ... */ } class MacTextBox extends TextBox { /* ... */ }现在核心来了抽象工厂接口。它声明了一组创建不同产品的方法。// 抽象工厂接口 class GUIFactory { createButton() { throw new Error(‘Method “createButton()” must be implemented.’); } createTextBox() { throw new Error(‘Method “createTextBox()” must be implemented.’); } }接着实现针对不同风格的具体工厂// 具体工厂Windows 工厂 class WindowsFactory extends GUIFactory { createButton() { return new WindowsButton(); // 创建 Windows 系列产品 } createTextBox() { return new WindowsTextBox(); } } // 具体工厂Mac 工厂 class MacFactory extends GUIFactory { createButton() { return new MacButton(); // 创建 Mac 系列产品 } createTextBox() { return new MacTextBox(); } }客户端代码如何工作它只需要依赖抽象工厂和抽象产品。// 客户端代码 class Application { constructor(factory) { // 客户端只与抽象工厂和抽象产品交互 this.factory factory; this.button null; this.textBox null; } createUI() { this.button this.factory.createButton(); this.textBox this.factory.createTextBox(); } render() { this.button.render(); this.textBox.render(); } } // 程序入口根据配置或环境决定使用哪个工厂 let factory; if (process.platform ‘win32‘) { factory new WindowsFactory(); } else if (process.platform ‘darwin‘) { factory new MacFactory(); } else { throw new Error(‘Unsupported OS’); } const app new Application(factory); app.createUI(); app.render();魔法发生了Application类完全不知道WindowsButton或MacTextBox的存在。它只通过GUIFactory接口来创建按钮和文本框。只要传入不同的具体工厂整个应用的 UI 风格就会一键切换。新增一个 Linux 风格只需要新增LinuxButton、LinuxTextBox和LinuxFactory然后修改工厂选择逻辑即可Application类一行代码都不用改。踩坑提醒产品族的扩展性抽象工厂模式最大的优点在于保证了产品族的兼容性但它的缺点也很明显难以支持新种类的产品。比如如果现在要在 UI 套件中增加一个“复选框”CheckBox产品那么你需要修改GUIFactory接口增加createCheckBox方法并修改所有具体工厂类WindowsFactory、MacFactory来实现它。这违反了开闭原则。因此抽象工厂模式适用于那些产品种类相对稳定但产品族需要频繁切换的场景。在设计之初就需要对产品族的维度有清晰的预见。4. 建造者模式分步构建复杂对象想象一下你要创建一个House对象。一栋房子有地基、墙体、屋顶、门窗、水电管线等无数部件。如果用一个庞大的构造函数new House(foundation, walls, roof, doors, windows, plumbing, electrical...)不仅参数列表长得可怕而且很多参数可能有默认值或者构建顺序有依赖关系总不能先装门窗再砌墙。这就是建造者模式要解决的问题将一个复杂对象的构建与它的表示分离使得同样的构建过程可以创建不同的表示。建造者模式特别适合创建那些需要通过多个步骤、且步骤间可能灵活组合的复杂对象。4.1 经典建造者与链式调用标准的建造者模式包含四个角色产品Product最终要构建的复杂对象如House。抽象建造者Builder声明创建产品各个部件的抽象方法。具体建造者Concrete Builder实现抽象建造者接口定义具体的构建步骤和部件装配逻辑并提供获取最终产品的方法。指挥者Director负责安排建造步骤的顺序。客户端通过指挥者与建造者交互。我们来看一个简化版的House建造示例// 1. 产品 - 房子 class House { constructor() { this.foundation ‘’; this.walls ‘’; this.roof ‘’; this.hasGarage false; this.hasGarden false; } describe() { let description A house with ${this.foundation} foundation, ${this.walls} walls, and a ${this.roof} roof.; if (this.hasGarage) description ‘ It has a garage.’; if (this.hasGarden) description ‘ It has a garden.’; console.log(description); } } // 2. 抽象建造者 class HouseBuilder { constructor() { this.house new House(); } buildFoundation() {} buildWalls() {} buildRoof() {} buildGarage() {} buildGarden() {} getResult() { return this.house; } } // 3. 具体建造者 - 现代风格房子建造者 class ModernHouseBuilder extends HouseBuilder { buildFoundation() { this.house.foundation ‘reinforced concrete’; console.log(‘Building reinforced concrete foundation.’); } buildWalls() { this.house.walls ‘glass and steel’; console.log(‘Building walls with glass and steel.’); } buildRoof() { this.house.roof ‘flat’; console.log(‘Building a flat roof.’); } buildGarage() { this.house.hasGarage true; console.log(‘Building an attached garage.’); } } // 4. 指挥者 class ConstructionEngineer { constructor(builder) { this.builder builder; } constructHouse() { this.builder.buildFoundation(); this.builder.buildWalls(); this.builder.buildRoof(); this.builder.buildGarage(); // 可选步骤 // this.builder.buildGarden(); // 这次不建花园 return this.builder.getResult(); } } // 客户端使用 const modernBuilder new ModernHouseBuilder(); const engineer new ConstructionEngineer(modernBuilder); const myModernHouse engineer.constructHouse(); myModernHouse.describe(); // 输出A house with reinforced concrete foundation, glass and steel walls, and a flat roof. It has a garage.在这个模式中ConstructionEngineer指挥者知道建房子的标准流程先地基再墙再屋顶...但它不关心具体用什么材料建。ModernHouseBuilder具体建造者知道如何用现代材料建造每一个部件。构建过程指挥者与部件实现建造者完全分离。要建一栋乡村木屋只需要创建一个CabinHouseBuilder然后传给同一个ConstructionEngineer即可。4.2 流式接口建造者更优雅的实践在实际开发中特别是 Java 和 JavaScript 领域一种被称为“流式接口”或“链式调用”的建造者变体更为流行。它省略了指挥者角色将构建步骤以方法链的形式呈现使客户端代码更加直观。class House { // ... 同上 ... } class HouseBuilder { constructor() { this.house new House(); } withFoundation(foundation) { this.house.foundation foundation; return this; // 关键返回 this实现链式调用 } withWalls(walls) { this.house.walls walls; return this; } withRoof(roof) { this.house.roof roof; return this; } withGarage() { this.house.hasGarage true; return this; } withGarden() { this.house.hasGarden true; return this; } build() { return this.house; } } // 客户端使用像说话一样构建对象 const myHouse new HouseBuilder() .withFoundation(‘concrete’) .withWalls(‘brick’) .withRoof(‘tile’) .withGarage() .build(); myHouse.describe();这种写法非常清晰可读性极强并且允许灵活选择构建哪些部件比如可以不调用.withGarage()。许多开源库的配置对象构建都采用这种方式例如axios.create({ baseURL: ‘...‘, timeout: 5000 })的内部实现思想就与此类似。经验之谈建造者 vs. 多参数构造函数当一个对象有超过4个以上的可选参数或者某些参数之间存在依赖或约束时就应该考虑使用建造者模式。它避免了“伸缩构造函数”反模式即提供多个不同参数数量的构造函数也避免了 Java 中常见的“JavaBeans”模式即先无参构造再一堆 setter所带来的对象状态不一致问题。建造者模式确保了对象在构建完毕调用build()之前始终处于一个有效或中间状态而一旦构建完成就是不可变的通常这更安全。5. 原型模式通过克隆来创建对象有些时候创建一个新对象的成本很高——可能是需要复杂的计算需要从数据库或网络加载大量数据或者只是单纯地耗时很长。如果我们需要多个相似或略有不同的对象每次都从头开始创建显然不划算。原型模式提供了另一种思路通过复制克隆一个现有实例来创建新对象。在支持原型模式的语言中如 JavaScript对象通常有一个“原型”链。但设计模式中的原型模式更强调一种显式的克隆行为。5.1 深拷贝与浅拷贝的陷阱实现原型模式的关键是实现一个clone方法。这里最大的坑在于深拷贝与浅拷贝。// 一个简单的原型示例 class Prototype { constructor(name, data) { this.name name; this.data data; // 假设 data 是一个对象或数组 this.createdAt new Date(); } // 浅拷贝克隆 shallowClone() { const clone Object.create(Object.getPrototypeOf(this)); Object.assign(clone, this); return clone; } // 深拷贝克隆简易版仅用于演示 deepClone() { const clone Object.create(Object.getPrototypeOf(this)); // 递归或使用其他深拷贝库如 lodash 的 _.cloneDeep clone.name this.name; clone.data JSON.parse(JSON.stringify(this.data)); // 简易深拷贝有局限性 clone.createdAt new Date(this.createdAt.getTime()); return clone; } log() { console.log(Name: ${this.name}, Data: ${JSON.stringify(this.data)}, Created: ${this.createdAt.toISOString()}); } } // 使用 const original new Prototype(‘Original‘, { items: [1, 2, 3] }); original.log(); const shallowCopy original.shallowClone(); shallowCopy.name ‘Shallow Copy‘; shallowCopy.data.items.push(4); // 修改引用类型属性 const deepCopy original.deepClone(); deepCopy.name ‘Deep Copy‘; deepCopy.data.items.push(5); console.log(‘--- After modifications ---‘); original.log(); // data.items 变成了 [1,2,3,4]被浅拷贝影响了。 shallowCopy.log(); // data.items 是 [1,2,3,4] deepCopy.log(); // data.items 是 [1,2,3,5]独立于原对象。浅拷贝只复制对象的第一层属性。如果属性是引用类型如对象、数组那么拷贝的只是引用地址新旧对象会共享这个引用修改其中一个会影响另一个。这通常不是我们想要的“克隆”。深拷贝会递归复制所有层级的属性创建一个完全独立的新对象。但实现一个健壮的深拷贝需要考虑循环引用、函数、特殊对象如 Date, RegExp等问题通常建议使用成熟的库如 Lodash 的_.cloneDeep。避坑指南原型模式的应用场景与选择资源优化当直接创建对象的成本如IO、计算远高于复制一个已有对象时。例如一个游戏场景中需要大量属性相同、只有位置不同的怪物可以先创建一个“原型”怪物然后克隆它并修改位置。避免子类爆炸工厂方法模式通过子类来创建不同产品。但如果产品种类繁多且差异仅在状态而非结构例如不同颜色的同一款汽车为每种颜色创建一个子类会导致“类爆炸”。此时可以用一个原型对象通过克隆并修改颜色属性来创建新对象。运行时动态配置对象的部分属性需要在运行时从复杂配置中加载。可以先创建一个加载好配置的原型对象后续对象通过克隆它来获得相同配置避免重复加载。在 JavaScript 中原型模式几乎内置于语言本身Object.create()。但在使用clone时务必想清楚你需要的是深拷贝还是浅拷贝。对于包含嵌套引用类型的对象默认的浅拷贝往往是 bug 的根源。6. 单例模式确保全局唯一访问点单例模式可能是最容易被误解和滥用的模式。它的意图很明确保证一个类只有一个实例并提供一个全局访问点。这常用于管理共享资源如数据库连接池、线程池、缓存、日志记录器、应用程序的配置对象等。6.1 懒汉式与饿汉式的实现与线程安全单例的实现有多种变体主要区别在于实例化的时机和是否线程安全。1. 饿汉式Eager Initialization实例在类加载时就创建。优点是实现简单且天生线程安全因为类加载过程是线程安全的。缺点是如果这个实例非常耗资源但程序又可能用不到它就会造成资源浪费。// Java 示例 - 饿汉式 public class EagerSingleton { // 类加载时即初始化 private static final EagerSingleton INSTANCE new EagerSingleton(); // 私有构造函数防止外部 new private EagerSingleton() { System.out.println(“EagerSingleton instance created.”); } // 全局访问点 public static EagerSingleton getInstance() { return INSTANCE; } }2. 懒汉式Lazy Initialization实例在第一次被请求时才创建。这避免了不必要的资源占用但带来了线程安全问题。// Java 示例 - 基础懒汉式非线程安全 public class LazySingleton { private static LazySingleton instance; private LazySingleton() { System.out.println(“LazySingleton instance created.”); } public static LazySingleton getInstance() { if (instance null) { // 多线程环境下多个线程可能同时通过这个检查 instance new LazySingleton(); } return instance; } }上述懒汉式在多线程环境下会创建多个实例。为了解决这个问题需要加锁。// 懒汉式 - 线程安全版同步方法性能较差 public class ThreadSafeLazySingleton { private static ThreadSafeLazySingleton instance; private ThreadSafeLazySingleton() {} public static synchronized ThreadSafeLazySingleton getInstance() { if (instance null) { instance new ThreadSafeLazySingleton(); } return instance; } }同步方法虽然安全但每次调用getInstance()都要加锁性能有损耗。于是有了“双重检查锁定”Double-Checked Locking的优化方案。// 懒汉式 - 双重检查锁定DCL public class DCLSingleton { // 使用 volatile 关键字确保 instance 的可见性和有序性防止指令重排 private static volatile DCLSingleton instance; private DCLSingleton() {} public static DCLSingleton getInstance() { if (instance null) { // 第一次检查避免不必要的同步 synchronized (DCLSingleton.class) { if (instance null) { // 第二次检查确保只有一个线程创建实例 instance new DCLSingleton(); } } } return instance; } }3. 静态内部类Holder方式这是 Java 中实现懒加载且线程安全的优雅方式利用了类加载机制。public class HolderSingleton { private HolderSingleton() {} // 静态内部类在第一次被引用时才加载 private static class SingletonHolder { private static final HolderSingleton INSTANCE new HolderSingleton(); } public static HolderSingleton getInstance() { return SingletonHolder.INSTANCE; // 这里才会触发 SingletonHolder 的加载和 INSTANCE 的初始化 } }4. 枚举Enum方式在 Java 中枚举类型本身就是单例的并且能防止反射和反序列化破坏单例是最推荐的方式。public enum EnumSingleton { INSTANCE; // 唯一的实例 public void doSomething() { // ... } } // 使用EnumSingleton.INSTANCE.doSomething();6.2 JavaScript 中的单例实践在 JavaScript 中实现单例更加灵活但也更容易出错。最常见的方式是利用模块化ES6 Module或对象字面量。// 方式1对象字面量最简单的单例 const configSingleton { apiUrl: ‘https://api.example.com‘, timeout: 5000, getSetting(key) { return this[key]; } }; // 这天然就是一个单例因为对象字面量在加载时创建。 // 方式2闭包与立即执行函数表达式IIFE const DatabaseConnection (function() { let instance null; // 私有变量存储唯一实例 function createConnection() { // 模拟一个耗时的连接创建过程 console.log(‘Creating new database connection...‘); return { query(sql) { console.log(Executing: ${sql}); } }; } return { getInstance: function() { if (!instance) { instance createConnection(); } return instance; } }; })(); // 使用 const conn1 DatabaseConnection.getInstance(); // 输出Creating new database connection... const conn2 DatabaseConnection.getInstance(); // 无输出返回已创建的实例 console.log(conn1 conn2); // true在 ES6 和 Node.js 环境中模块系统本身就是天然的单例管理器。一个模块无论被import或require多少次都只会被执行和初始化一次导出的对象就是单例。// dbConnection.js let connection null; export function getConnection() { if (!connection) { connection createConnection(); // 假设的创建函数 } return connection; } // app.js import { getConnection } from ‘./dbConnection.js‘; const conn1 getConnection(); const conn2 getConnection(); // conn1 和 conn2 是同一个对象重要警告单例模式的滥用与替代单例模式因其“全局访问”的特性很容易被滥用导致代码紧耦合、难以测试因为依赖隐藏的全局状态、违反单一职责原则。在现代软件开发中依赖注入Dependency Injection, DI容器如 Spring Framework, Angular 中的 Injector已经很大程度上取代了手写单例模式。容器负责管理对象的生命周期单例、原型等并将依赖“注入”到需要它的类中。这样类本身不需要关心依赖是否是单例它只通过构造函数或属性声明依赖由容器来保证提供正确的实例。这大大提高了代码的可测试性和可维护性。因此除非在非常简单的场景或框架底层否则应优先考虑使用 DI 容器来管理单例而不是自己手动实现一个全局的getInstance()。