一、为什么需要模块化设计模式?

在开发电商系统的商品详情页时,我曾遇到这样的尴尬场景:不同工程师开发的弹窗组件互相覆盖全局变量,导致点击某个按钮会同时弹出三个不同样式的提示框。这种混乱的根源就在于代码组织缺乏规范,而模块化设计模式正是解决这类问题的金钥匙。

模块化设计模式的核心价值在于隔离代码、规范通信、降低耦合。就像乐高积木的标准化接口设计,它们能让不同模块像积木一样灵活拼接。下面我们通过三个典型模式,看看如何打造健壮的JavaScript架构。


二、单例模式:唯一真理之源

2.1 模式定义与应用场景

当我们想要确保某个类只有一个实例存在时,单例模式便闪亮登场。比如:

  • 全局配置管理
  • 浏览器环境中的window对象
  • 应用状态管理(如Redux Store)
  • 日志记录系统

以下是一个本地存储管理器的实现示例(技术栈:ES6+):

class StorageManager {
  constructor() {
    if (!StorageManager.instance) {
      this._cache = new Map();
      StorageManager.instance = this;
    }
    return StorageManager.instance;
  }

  // 统一存储入口
  setItem(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
    this._cache.set(key, value);
  }

  // 带缓存的读取
  getItem(key) {
    if (this._cache.has(key)) {
      return this._cache.get(key);
    }
    const value = JSON.parse(localStorage.getItem(key));
    this._cache.set(key, value);
    return value;
  }

  // 删除操作同步缓存
  removeItem(key) {
    localStorage.removeItem(key);
    this._cache.delete(key);
  }
}

// 创建单例的正确方式
const storage = new StorageManager();
Object.freeze(storage); // 防止意外修改

// 使用方法:
storage.setItem('userTheme', { mode: 'dark', color: '#2c3e50' });
console.log(storage.getItem('userTheme')); // {mode: 'dark', color: '#2c3e50'}

关键技术点:

  1. 通过静态属性instance检测是否存在实例
  2. 构造函数返回已存在的实例
  3. 结合Map实现内存缓存加速
  4. Object.freeze防止意外修改

2.2 优缺点分析

优势

  • 严格控制实例数量,节省内存
  • 全局访问点方便管理
  • 避免重复初始化消耗

劣势

  • 违反单一职责原则(同时管理实例和业务)
  • 单元测试时需要特殊处理
  • 可能演变成"上帝对象"

三、工厂模式:造物主的艺术

3.1 模式变体与实战示例

在开发可视化报表系统时,我们需要根据数据类型(柱状图、折线图、饼图)创建不同的图表组件。这时工厂模式就像3D打印机,将创建逻辑与使用逻辑解耦。

下面实现一个UI组件工厂(技术栈:TypeScript 4.0+):

// 组件基础接口
interface ChartComponent {
  render(container: HTMLElement): void;
  update(data: unknown): void;
}

// 具体产品实现
class BarChart implements ChartComponent {
  constructor(private data: number[]) {}

  render(container: HTMLElement) {
    const canvas = document.createElement('canvas');
    // 使用Chart.js渲染柱状图...
    container.appendChild(canvas);
  }

  update(data: number[]) {
    // 更新图表数据逻辑...
  }
}

class PieChart implements ChartComponent {
  // 类似BarChart的实现...
}

// 核心工厂类
class ChartFactory {
  static create(type: 'bar' | 'pie', data: unknown): ChartComponent {
    switch(type) {
      case 'bar':
        return new BarChart(data as number[]);
      case 'pie':
        return new PieChart(data as Record<string, number>);
      default:
        throw new Error('未知图表类型');
    }
  }
}

// 使用示例:
const salesData = [120, 200, 150, 80];
const chart = ChartFactory.create('bar', salesData);
chart.render(document.getElementById('chart-container')!);

模式亮点:

  1. 客户端无需关心具体实现类
  2. 新增图表类型只需扩展工厂
  3. 统一了组件的创建接口

3.2 应用场景选择指南

选择工厂模式的典型信号:

  • 创建逻辑包含复杂条件判断
  • 需要支持运行时动态创建
  • 系统需要独立于产品类型
  • 需要统一产品的创建约束

四、依赖注入:高内聚的粘合剂

4.1 原理与实现技巧

在开发用户管理系统时,用户服务需要依赖数据库连接和日志服务。传统的直接依赖会导致高度耦合,就像把电线直接焊死在电路板上。

下面演示基于构造函数注入的实现(技术栈:Node.js + Express):

// 依赖项定义
class DatabaseConnection {
  constructor(config) {
    // 初始化数据库连接...
  }

  query(sql) {
    // 执行查询...
  }
}

class Logger {
  log(message) {
    console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
  }
}

// 用户服务类(被注入者)
class UserService {
  constructor(db, logger) {
    this.db = db;
    this.logger = logger;
  }

  async getUsers() {
    this.logger.log('获取用户列表');
    return this.db.query('SELECT * FROM users');
  }
}

// 依赖注入容器
class Container {
  constructor() {
    this.services = {};
  }

  register(name, creator) {
    this.services[name] = creator;
  }

  resolve(name) {
    const creator = this.services[name];
    return creator(this);
  }
}

// 配置容器
const container = new Container();
container.register('db', () => new DatabaseConnection({host: 'localhost'}));
container.register('logger', () => new Logger());
container.register('userService', (c) => 
  new UserService(c.resolve('db'), c.resolve('logger'))
);

// 使用示例:
const userService = container.resolve('userService');
userService.getUsers();

技术关键:

  1. 将依赖项声明在构造函数中
  2. 容器统一管理依赖生命周期
  3. 按需解析依赖关系
  4. 支持依赖替换(测试时可注入Mock对象)

4.2 进阶应用:结合装饰器

使用TypeScript装饰器实现更优雅的方案:

import 'reflect-metadata';

const SERVICE_TOKEN = Symbol('di:service');

// 装饰器定义
function Injectable() {
  return function(target: any) {
    Reflect.defineMetadata(SERVICE_TOKEN, true, target);
  };
}

// 用户服务类
@Injectable()
class UserService {
  constructor(
    private db: DatabaseConnection,
    private logger: Logger
  ) {}
}

// 容器实现
class DIContainer {
  private instances = new Map();

  register<T>(token: symbol, provider: new (...args: any[]) => T) {
    this.instances.set(token, provider);
  }

  resolve<T>(token: symbol): T {
    const Provider = this.instances.get(token);
    const dependencies = Reflect.getMetadata('design:paramtypes', Provider) || [];
    const args = dependencies.map((Dep: any) => this.resolve(Dep));
    return new Provider(...args);
  }
}

五、应用场景与技术选型

5.1 单例模式适用领域

  • 全局资源管理:配置中心、主题管理
  • 跨模块共享:状态管理器、WebSocket连接
  • 性能敏感场景:缓存系统、数据库连接池

5.2 工厂模式最佳实践

  • 动态环境:插件系统、多环境适配
  • 产品系列管理:UI组件库、支付渠道切换
  • 复杂对象构造:复合文档生成、游戏实体创建

5.3 依赖注入的理想战场

  • 企业级应用:微服务架构、模块化系统
  • 测试驱动开发:Mock依赖注入
  • 可扩展架构:插件系统、中间件栈

六、模式对比与注意事项

维度 单例模式 工厂模式 依赖注入
主要目的 控制实例数量 封装创建逻辑 解耦依赖关系
适用阶段 运行时初始化 对象创建时 依赖配置阶段
复杂度 中等 较高
测试难度 需要重置实例 容易Mock 天然支持测试
典型风险 全局状态污染 类型爆炸 配置复杂度

通用注意事项

  1. 避免在单例中持有易变状态
  2. 工厂方法应保持单一职责
  3. 依赖注入需警惕循环依赖
  4. 控制模式嵌套层级

七、总结与升华

模块化设计如同建筑中的结构力学,单例模式是承重墙,工厂模式是预制件,依赖注入是钢筋骨架。在复杂的前端应用中:

  • 项目初期:优先使用工厂模式解耦创建逻辑
  • 中期扩展:引入依赖注入提升可测试性
  • 后期优化:针对关键服务使用单例管控资源

这三种模式并非对立选择,而是可以组合使用的工具箱。例如用工厂生产单例对象,或在依赖注入容器中注册工厂方法。真正的高手知道何时该遵循模式,何时需要打破常规。