JavaScript设计模式

JavaScript设计模式示意

设计模式是软件开发的智慧结晶,是前辈们在长期实践中总结出的可复用解决方案。在JavaScript开发中,恰当地运用设计模式可以让代码更加优雅、可维护、可扩展。作为一个在代码丛林中摸爬滚打多年的开发者,我深刻体会到:好的设计模式就像魔法咒语,能让混乱的代码变得井然有序。

为什么需要设计模式?

设计模式并非银弹,它们是解决问题的工具而非目的本身。在合适的场景使用合适的模式,能够带来诸多好处:代码复用性提高、可读性增强、维护成本降低、团队协作更顺畅。但过度使用或生搬硬套,反而会让简单的问题复杂化。

JavaScript作为一门灵活的动态语言,实现设计模式的方式与其他语言有所不同。我们可以利用闭包、原型链、高阶函数等特性,以更加简洁优雅的方式实现经典模式。

单例模式

单例模式确保一个类只有一个实例,并提供一个全局访问点。在前端开发中,单例模式常用于管理全局状态、配置对象、弹窗组件等场景。

基础实现

class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }
    Singleton.instance = this;
    this.data = {};
  }
  
  setData(key, value) {
    this.data[key] = value;
  }
  
  getData(key) {
    return this.data[key];
  }
}

// 使用
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true

使用闭包实现

const Singleton = (function() {
  let instance;
  
  function createInstance() {
    return {
      data: {},
      set(key, value) {
        this.data[key] = value;
      },
      get(key) {
        return this.data[key];
      }
    };
  }
  
  return {
    getInstance() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();

const a = Singleton.getInstance();
const b = Singleton.getInstance();
console.log(a === b); // true

实际应用:全局状态管理

class AppState {
  constructor() {
    if (AppState.instance) {
      return AppState.instance;
    }
    
    this.state = {
      user: null,
      theme: 'light',
      notifications: []
    };
    
    this.listeners = [];
    
    AppState.instance = this;
  }
  
  subscribe(listener) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }
  
  setState(newState) {
    this.state = { ...this.state, ...newState };
    this.listeners.forEach(listener => listener(this.state));
  }
  
  getState() {
    return this.state;
  }
}

const appState = new AppState();

工厂模式

工厂模式提供了一种创建对象的最佳方式,在创建对象时不会对客户端暴露创建逻辑。这种模式特别适合需要根据条件创建不同类型对象的场景。

简单工厂

class Button {
  constructor(type) {
    this.type = type;
  }
  
  render() {
    return ``;
  }
}

class ButtonFactory {
  static create(type) {
    switch(type) {
      case 'primary':
        return new Button('btn-primary');
      case 'secondary':
        return new Button('btn-secondary');
      case 'danger':
        return new Button('btn-danger');
      default:
        throw new Error('未知按钮类型');
    }
  }
}

const primaryBtn = ButtonFactory.create('primary');
const dangerBtn = ButtonFactory.create('danger');

抽象工厂

// 抽象产品
class UIComponent {
  render() {
    throw new Error('子类必须实现render方法');
  }
}

// 具体产品:按钮
class Button extends UIComponent {
  constructor(text) {
    super();
    this.text = text;
  }
  
  render() {
    return ``;
  }
}

// 具体产品:输入框
class Input extends UIComponent {
  constructor(placeholder) {
    super();
    this.placeholder = placeholder;
  }
  
  render() {
    return ``;
  }
}

// 抽象工厂
class UIFactory {
  createButton() {
    throw new Error('子类必须实现createButton方法');
  }
  
  createInput() {
    throw new Error('子类必须实现createInput方法');
  }
}

// 具体工厂:桌面版UI
class DesktopUIFactory extends UIFactory {
  createButton() {
    return new Button('桌面按钮');
  }
  
  createInput() {
    return new Input('请输入内容');
  }
}

// 具体工厂:移动版UI
class MobileUIFactory extends UIFactory {
  createButton() {
    return new Button('移动按钮');
  }
  
  createInput() {
    return new Input('点击输入');
  }
}

// 使用
function createUI(factory) {
  const button = factory.createButton();
  const input = factory.createInput();
  return { button, input };
}

const desktopUI = createUI(new DesktopUIFactory());
const mobileUI = createUI(new MobileUIFactory());

观察者模式

观察者模式示意

观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这是前端开发中最常用的模式之一。

基础实现

class Subject {
  constructor() {
    this.observers = [];
  }
  
  subscribe(observer) {
    this.observers.push(observer);
    return () => {
      this.observers = this.observers.filter(o => o !== observer);
    };
  }
  
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }
  
  update(data) {
    console.log(`${this.name} 收到通知:`, data);
  }
}

// 使用
const subject = new Subject();
const observer1 = new Observer('观察者1');
const observer2 = new Observer('观察者2');

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

subject.notify('重要消息');

实际应用:事件总线

class EventBus {
  constructor() {
    this.events = {};
  }
  
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    
    // 返回取消订阅函数
    return () => {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    };
  }
  
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(...args));
    }
  }
  
  once(event, callback) {
    const wrapper = (...args) => {
      callback(...args);
      this.off(event, wrapper);
    };
    this.on(event, wrapper);
  }
  
  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
}

// 全局事件总线
const eventBus = new EventBus();

// 组件A订阅事件
eventBus.on('user-login', (user) => {
  console.log('用户登录:', user);
});

// 组件B触发事件
eventBus.emit('user-login', { name: '张三', id: 1 });

发布订阅模式

发布订阅模式与观察者模式相似,但有一个关键区别:发布者和订阅者之间通过消息代理进行通信,两者不直接接触。这种模式更加解耦,特别适合复杂的消息传递场景。

class PubSub {
  constructor() {
    this.topics = {};
    this.subUid = -1;
  }
  
  subscribe(topic, func) {
    if (!this.topics[topic]) {
      this.topics[topic] = [];
    }
    
    const token = (++this.subUid).toString();
    this.topics[topic].push({
      token,
      func
    });
    
    return token;
  }
  
  publish(topic, ...args) {
    if (!this.topics[topic]) {
      return false;
    }
    
    setTimeout(() => {
      this.topics[topic].forEach(subscriber => {
        subscriber.func(...args);
      });
    }, 0);
    
    return true;
  }
  
  unsubscribe(token) {
    for (const topic in this.topics) {
      if (this.topics[topic]) {
        this.topics[topic] = this.topics[topic].filter(
          subscriber => subscriber.token !== token
        );
      }
    }
    return this;
  }
}

// 使用
const pubsub = new PubSub();

// 订阅消息
const token = pubsub.subscribe('news', (data) => {
  console.log('收到新闻:', data.title);
});

// 发布消息
pubsub.publish('news', {
  title: '重大突破!',
  content: '...'
});

// 取消订阅
pubsub.unsubscribe(token);

模块模式

模块模式使用闭包来封装私有状态和方法,只暴露公共接口。在ES6模块出现之前,这是JavaScript中实现封装的主要方式。

const UserModule = (function() {
  // 私有变量和方法
  const users = [];
  let currentId = 0;
  
  function generateId() {
    return ++currentId;
  }
  
  function validateUser(user) {
    return user.name && user.email;
  }
  
  // 公共接口
  return {
    addUser(user) {
      if (!validateUser(user)) {
        throw new Error('无效的用户数据');
      }
      const newUser = {
        id: generateId(),
        ...user,
        createdAt: new Date()
      };
      users.push(newUser);
      return newUser;
    },
    
    getUser(id) {
      return users.find(u => u.id === id);
    },
    
    getAllUsers() {
      return [...users];
    },
    
    removeUser(id) {
      const index = users.findIndex(u => u.id === id);
      if (index > -1) {
        return users.splice(index, 1)[0];
      }
      return null;
    }
  };
})();

// 使用
const user = UserModule.addUser({
  name: '张三',
  email: 'zhangsan@example.com'
});

策略模式

策略模式定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换。这种模式让算法独立于使用它的客户而变化。

// 策略对象
const strategies = {
  S: (salary) => salary * 4,
  A: (salary) => salary * 3,
  B: (salary) => salary * 2,
  C: (salary) => salary * 1.5
};

// 上下文
class BonusCalculator {
  constructor() {
    this.strategy = null;
    this.salary = 0;
  }
  
  setStrategy(strategy) {
    this.strategy = strategy;
  }
  
  setSalary(salary) {
    this.salary = salary;
  }
  
  calculate() {
    if (!this.strategy) {
      throw new Error('未设置策略');
    }
    return strategies[this.strategy](this.salary);
  }
}

// 使用
const calculator = new BonusCalculator();
calculator.setSalary(10000);
calculator.setStrategy('S');
console.log(calculator.calculate()); // 40000

calculator.setStrategy('B');
console.log(calculator.calculate()); // 20000

装饰器模式

装饰器模式动态地给对象添加一些额外的职责。相比继承,装饰器模式更加灵活,可以在运行时决定添加什么功能。

// 基础组件
class Coffee {
  cost() {
    return 10;
  }
  
  description() {
    return '普通咖啡';
  }
}

// 装饰器基类
class CoffeeDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
  
  cost() {
    return this.coffee.cost();
  }
  
  description() {
    return this.coffee.description();
  }
}

// 具体装饰器
class MilkDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 3;
  }
  
  description() {
    return this.coffee.description() + ' + 牛奶';
  }
}

class SugarDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 1;
  }
  
  description() {
    return this.coffee.description() + ' + 糖';
  }
}

class WhipDecorator extends CoffeeDecorator {
  cost() {
    return this.coffee.cost() + 5;
  }
  
  description() {
    return this.coffee.description() + ' + 奶油';
  }
}

// 使用
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);

console.log(coffee.description()); // 普通咖啡 + 牛奶 + 糖
console.log(coffee.cost()); // 14

最佳实践与注意事项

  1. 不要过度设计:简单的问题用简单的方案解决,设计模式是用来解决复杂问题的
  2. 理解场景再选模式:每个模式都有适用场景,生搬硬套只会增加复杂度
  3. 组合优于继承:在JavaScript中,组合和代理往往比继承更灵活
  4. 利用语言特性:JavaScript的闭包、高阶函数等特性可以实现更简洁的模式实现
  5. 保持单一职责:每个模块、类应该只有一个变化的理由
  6. 注重代码可读性:模式的目的是让代码更好维护,不是炫技

总结

设计模式是软件工程领域的宝贵财富,掌握它们能让你在面对复杂问题时游刃有余。但记住,模式是工具不是目的,代码的最终目标是解决问题、易于理解和维护。在实际开发中,根据具体场景选择合适的模式,而不是为了使用模式而使用模式。

JavaScript的灵活性让我们能够以多种方式实现这些模式,选择最适合团队和项目的方式才是关键。希望这篇文章能帮助你建立对设计模式的深入理解,在代码之路上越走越远。