一、为什么需要设计模式

在日常开发中,我们经常会遇到一些似曾相识的问题。比如多个组件需要共享数据,或者需要控制某个对象的创建过程。这时候如果每次都从头开始写代码,不仅效率低下,而且容易产生bug。设计模式就像是经过千锤百炼的解决方案模板,能帮助我们快速应对这些常见场景。

举个例子,电商网站的商品详情页需要展示库存信息,这个数据会被添加到购物车组件、收藏组件等多个地方。如果每个组件都单独请求库存接口,不仅会造成资源浪费,还可能导致数据不一致。这时候,观察者模式就能派上用场了。

二、单例模式实战

单例模式确保一个类只有一个实例,这在需要全局唯一对象的场景非常有用。比如前端的状态管理、日志记录器等。

// 技术栈:JavaScript ES6
class Logger {
  constructor() {
    if (!Logger.instance) {
      this.logs = [];
      Logger.instance = this;
    }
    return Logger.instance;
  }

  log(message) {
    this.logs.push(message);
    console.log(`[LOG]: ${message}`);
  }

  printLogCount() {
    console.log(`共 ${this.logs.length} 条日志`);
  }
}

// 使用示例
const logger1 = new Logger();
const logger2 = new Logger();

logger1.log('第一次日志');
logger2.log('第二次日志');

console.log(logger1 === logger2); // true,说明是同一个实例
logger1.printLogCount(); // 共 2 条日志

这个实现有几个关键点:

  1. 通过静态属性instance保存唯一实例
  2. 构造函数中判断是否已存在实例
  3. 提供全局访问点

应用场景:

  • 全局状态管理
  • 日志记录器
  • 浏览器中的window对象

注意事项:

  • 在多线程环境下需要额外处理(前端一般不需要)
  • 过度使用会导致代码耦合度高

三、观察者模式实战

观察者模式定义了对象间一对多的依赖关系,当一个对象状态改变时,所有依赖它的对象都会得到通知。这在事件处理系统、数据绑定等场景非常有用。

// 技术栈:JavaScript ES6
class Subject {
  constructor() {
    this.observers = [];
  }

  subscribe(observer) {
    this.observers.push(observer);
  }

  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log(`收到通知:${data}`);
  }
}

// 使用示例
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.subscribe(observer1);
subject.subscribe(observer2);

subject.notify('数据更新了!');
// 输出:
// 收到通知:数据更新了!
// 收到通知:数据更新了!

subject.unsubscribe(observer1);
subject.notify('再次更新');
// 输出:
// 收到通知:再次更新

实际项目中,我们可以用它来实现:

  1. 自定义事件系统
  2. 响应式数据绑定
  3. 实时通知功能

优点:

  • 松耦合,主题和观察者可以独立变化
  • 支持广播通信

缺点:

  • 如果观察者过多,通知会耗时
  • 循环依赖可能导致系统崩溃

四、策略模式实战

策略模式定义了一系列算法,并将每个算法封装起来,使它们可以互相替换。这在需要根据不同条件执行不同逻辑的场景特别有用。

// 技术栈:JavaScript ES6
// 定义策略类
const strategies = {
  'level1'(bonus) {
    return bonus * 0.1;
  },
  'level2'(bonus) {
    return bonus * 0.2;
  },
  'level3'(bonus) {
    return bonus * 0.3;
  }
};

// 上下文类
class BonusCalculator {
  constructor() {
    this.strategy = null;
    this.bonus = 0;
  }

  setStrategy(strategy) {
    this.strategy = strategies[strategy];
  }

  setBonus(bonus) {
    this.bonus = bonus;
  }

  calculate() {
    if (!this.strategy) {
      throw new Error('请先设置策略');
    }
    return this.strategy(this.bonus);
  }
}

// 使用示例
const calculator = new BonusCalculator();
calculator.setBonus(10000);
calculator.setStrategy('level2');
console.log(calculator.calculate()); // 2000

应用场景:

  1. 表单验证规则
  2. 促销折扣计算
  3. 算法选择

优点:

  • 避免多重条件判断
  • 易于扩展新策略
  • 算法可以复用

注意事项:

  • 会增加策略类数量
  • 客户端需要了解所有策略

五、装饰器模式实战

装饰器模式允许向现有对象添加新功能,同时又不改变其结构。这在需要动态添加功能的场景非常有用。

// 技术栈:JavaScript ES6 + TypeScript装饰器语法
// 简单的装饰器函数
function log(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`调用 ${name} 参数:${args}`);
    return original.apply(this, args);
  };
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

// 使用示例
const calc = new Calculator();
console.log(calc.add(2, 3));
// 输出:
// 调用 add 参数:2,3
// 5

实际应用:

  1. 日志记录
  2. 权限控制
  3. 性能监控

优点:

  • 比继承更灵活
  • 可以动态添加/删除功能
  • 符合开闭原则

缺点:

  • 会产生许多小对象
  • 过度使用会使程序变得复杂

六、总结

设计模式不是银弹,但确实是解决特定问题的有效工具。在实际项目中,我们常常需要根据具体场景灵活运用甚至组合多种模式。比如一个电商系统可能同时使用:

  • 单例模式管理购物车
  • 观察者模式处理库存变化
  • 策略模式计算不同会员等级的折扣
  • 装饰器模式添加日志和权限控制

记住,设计模式的本质是提高代码的可维护性和可扩展性,而不是为了模式而模式。当简单的代码就能解决问题时,不要过度设计。

最后,分享一个选择设计模式的小技巧:当你发现自己在写相似的代码结构第三次时,可能就是时候考虑引入适当的设计模式了。