js开发中常见的设计模式与代码示例

279 阅读11分钟

适配器模式

适配器模式(Adapter Pattern)的主要作用是将一个类的接口转换成客户希望的另一个接口,以解决新旧系统接口不兼容的问题。下面是一个简单的适配器模式的代码示例。

假设我们有一个电子词典类 Dictionary,其中有一个方法 translate,可以根据给定的单词返回翻译结果:

class Dictionary {
  translate(word) {
    switch (word) {
      case "apple":
        return "苹果";
      case "banana":
        return "香蕉";
      case "cherry":
        return "樱桃";
      default:
        return "未知";
    }
  }
}

现在我们有一个需要调用这个词典的外部系统,但是它需要的接口是一个对象,其中包含了一个名为 query 的方法,可以根据给定的参数返回查询结果。我们可以通过适配器模式来实现这个转换:

class DictionaryAdapter {
  constructor(dictionary) {
    this.dictionary = dictionary;
  }

  query(queryString) {
    const result = this.dictionary.translate(queryString);
    return { word: queryString, result };
  }
}

这个适配器类 DictionaryAdapter 接收一个 Dictionary 实例作为参数,在 query 方法中调用 Dictionary 实例的 translate 方法,并将返回值转换成需要的格式。这样我们就可以将 Dictionary 类的接口适配成外部系统需要的接口,从而实现接口的兼容。使用示例:

const dictionary = new Dictionary();
const adapter = new DictionaryAdapter(dictionary);

const result = adapter.query("apple");
console.log(result); // { word: "apple", result: "苹果" }

策略模式

策略模式(Strategy Pattern)的主要作用是定义一系列算法,并将每个算法封装起来,使它们可以互相替换,以解决多种算法的选择问题。下面是一个简单的策略模式的代码示例。

假设我们有一个计算器类 Calculator,其中有一个方法 calculate,可以根据给定的操作符和操作数返回计算结果:

class Calculator {
  calculate(operator, operand1, operand2) {
    switch (operator) {
      case "+":
        return operand1 + operand2;
      case "-":
        return operand1 - operand2;
      case "*":
        return operand1 * operand2;
      case "/":
        return operand1 / operand2;
      default:
        return NaN;
    }
  }
}

现在我们想要支持多种计算方式,比如普通计算、取模计算等。我们可以使用策略模式来实现:

首先定义一个接口或抽象类,用于规范不同的计算策略:

class CalculationStrategy {
  calculate(operand1, operand2) {
    throw new Error("not implemented");
  }
}

然后针对不同的计算方式分别实现不同的策略类:

class NormalCalculationStrategy extends CalculationStrategy {
  calculate(operand1, operand2) {
    return operand1 + operand2;
  }
}

class ModuloCalculationStrategy extends CalculationStrategy {
  calculate(operand1, operand2) {
    return operand1 % operand2;
  }
}

最后在 Calculator 类中使用策略模式:

class Calculator {
  constructor(strategy) {
    this.strategy = strategy;
  }

  calculate(operand1, operand2) {
    return this.strategy.calculate(operand1, operand2);
  }
}

这个 Calculator 类接收一个 CalculationStrategy 实例作为参数,并在 calculate 方法中调用 CalculationStrategy 实例的 calculate 方法。这样我们就可以在不改变 Calculator 类的代码的前提下,动态地替换不同的 CalculationStrategy 实例,从而实现不同的计算方式。

工厂模式

工厂模式(Factory Pattern):用于创建对象,根据不同的参数返回不同的实例。常用于解决创建对象过程中的复杂性和灵活性问题。

当需要创建多个对象时,可以使用工厂模式,它允许我们使用相同的代码来创建不同类型的对象。以下是一个基本的 JavaScript 工厂模式示例:

// 定义一个工厂函数
function createPerson(name, age) {
  // 创建一个新对象
  let obj = {};

  // 添加属性和方法到新对象
  obj.name = name;
  obj.age = age;
  obj.introduce = function() {
    console.log('My name is ' + this.name + ', and I am ' + this.age + ' years old.');
  };

  // 返回新对象
  return obj;
}

// 使用工厂函数创建两个人对象
let person1 = createPerson('Tom', 25);
let person2 = createPerson('Jerry', 23);

// 调用对象方法
person1.introduce(); // 输出 "My name is Tom, and I am 25 years old."
person2.introduce(); // 输出 "My name is Jerry, and I am 23 years old."

在上面的示例中,我们定义了一个名为 createPerson 的工厂函数,它接受两个参数 name 和 age,用于创建一个新的人对象。在函数内部,我们创建了一个新对象 obj,并向其添加了 nameage 和 introduce 属性和方法。最后,我们返回新对象 obj

通过调用工厂函数 createPerson,我们可以创建多个人对象,每个对象都具有相同的属性和方法。我们可以通过调用对象的 introduce() 方法来展示对象的详细信息。

工厂模式创建对象和使用 class 类创建对象相比,有以下不足之处:

  1. 缺乏类型检查:在使用工厂模式创建对象时,创建的对象只是一个普通的 JavaScript 对象,并没有进行类型检查,因此无法保证创建的对象是否符合特定的类型或接口。
  2. 缺乏继承性:工厂模式创建的对象是通过调用工厂方法来创建的,因此无法继承其他对象的属性和方法。而使用 class 类创建对象,则可以通过继承来实现代码复用。
  3. 难以进行单元测试:工厂模式创建的对象通常是匿名的,无法在单元测试中进行测试。而使用 class 类创建对象,则可以通过类名来进行单元测试。
  4. 代码可读性较差:使用工厂模式创建的对象通常需要编写更多的代码,而且代码结构较为复杂,因此可读性较差。

总之,使用工厂模式创建对象和使用 class 类创建对象都有其优缺点。需要根据具体情况来选择最适合的方法来创建对象。

装饰器模式

装饰器模式(Decorator Pattern):动态地给一个对象添加额外的职责。常用于解决继承关系过于复杂的问题。

装饰器模式是一种结构型模式,它通过动态地给对象添加额外的职责来扩展其功能。在 JavaScript 中,装饰器模式可以通过函数和对象的组合来实现。以下是一个使用装饰器模式实现的 JavaScript 代码示例:

// 定义一个基础组件
class Component {
  operation() {
    return 'Component';
  }
}

// 定义一个装饰器基类
class Decorator {
  constructor(component) {
    this.component = component;
  }

  operation() {
    return this.component.operation();
  }
}

// 定义一个具体的装饰器
class ConcreteDecorator extends Decorator {
  constructor(component) {
    super(component);
  }

  operation() {
    return `ConcreteDecorator(${this.component.operation()})`;
  }
}

// 使用
const component = new Component();
const decoratedComponent = new ConcreteDecorator(component);
console.log(decoratedComponent.operation()); // Output: ConcreteDecorator(Component)

在这个示例中,Component 类是一个基础组件。Decorator 类是一个装饰器基类,它维护了一个指向被装饰对象的引用,并且实现了与被装饰对象相同的接口。ConcreteDecorator 是一个具体的装饰器,它包装了一个组件并添加了额外的功能。在主程序中,我们首先创建了一个基础组件 component,然后将其传递给一个具体的装饰器 decoratedComponent。当我们调用 decoratedComponent.operation() 时,它会调用被装饰对象的 operation() 方法,并在其结果前后添加字符串“ConcreteDecorator”和圆括号,从而实现了对组件的装饰。

观察者模式

观察者模式(Observer Pattern):定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会被通知并自动更新。常用于解决对象之间的联动问题。

以下是一个使用观察者模式实现的 TypeScript 代码示例:

interface Observer {
  update(data: any): void;
}

class Subject {
  private observers: Observer[] = [];
  private data: any;

  public setData(data: any) {
    this.data = data;
    this.notifyObservers();
  }

  public attach(observer: Observer) {
    this.observers.push(observer);
  }

  public detach(observer: Observer) {
    const index = this.observers.indexOf(observer);
    if (index >= 0) {
      this.observers.splice(index, 1);
    }
  }

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

class ConcreteObserverA implements Observer {
  public update(data: any) {
    console.log(`ConcreteObserverA received data: ${data}`);
  }
}

class ConcreteObserverB implements Observer {
  public update(data: any) {
    console.log(`ConcreteObserverB received data: ${data}`);
  }
}

// 用法:
const subject = new Subject();
const observerA = new ConcreteObserverA();
const observerB = new ConcreteObserverB();

subject.attach(observerA);
subject.attach(observerB);

subject.setData('Hello world!'); 
// Output: ConcreteObserverA received data: Hello world!, ConcreteObserverB received data: Hello world!

subject.detach(observerB);

subject.setData('Goodbye world!'); 
// Output: ConcreteObserverA received data: Goodbye world!

在这个示例中,Observer 接口定义了观察者的行为,包括 update 方法。Subject 类是被观察的主题,包括 setDataattachdetach 和 notifyObservers 方法。ConcreteObserverA 和 ConcreteObserverB 是具体的观察者实现。在主程序中,我们创建了一个 Subject 对象和两个观察者对象,然后将观察者对象附加到主题对象中。当主题对象的数据变化时,它会通知所有的观察者对象。在这个示例中,我们只是简单地将数据输出到控制台。

发布订阅模式

以下是一个使用发布订阅模式实现的 JavaScript 代码示例:

class PubSub {
  constructor() {
    this.events = {};
  }

  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }

    this.events[event].push(callback);
  }

  unsubscribe(event, callback) {
    if (!this.events[event]) {
      return;
    }

    const index = this.events[event].indexOf(callback);
    if (index >= 0) {
      this.events[event].splice(index, 1);
    }
  }

  publish(event, data) {
    if (!this.events[event]) {
      return;
    }

    this.events[event].forEach((callback) => {
      callback(data);
    });
  }
}

// Usage
const pubsub = new PubSub();

const callback1 = (data) => {
  console.log(`Callback1 received data: ${data}`);
};

const callback2 = (data) => {
  console.log(`Callback2 received data: ${data}`);
};

pubsub.subscribe('eventA', callback1);
pubsub.subscribe('eventA', callback2);

pubsub.publish('eventA', 'Hello world!'); // Output: Callback1 received data: Hello world!, Callback2 received data: Hello world!

pubsub.unsubscribe('eventA', callback2);

pubsub.publish('eventA', 'Goodbye world!'); // Output: Callback1 received data: Goodbye world!

在这个示例中,PubSub 类实现了发布订阅模式,并包括 subscribeunsubscribe 和 publish 方法。我们可以使用 subscribe 方法来订阅一个事件,并给它一个回调函数。当事件被发布时,所有订阅了该事件的回调函数都会被执行。我们可以使用 unsubscribe 方法来取消订阅一个事件。在主程序中,我们创建了一个 PubSub 对象,然后订阅了两个回调函数到 eventA 事件上。当 eventA 事件被发布时,这两个回调函数都会被执行。然后我们取消了一个回调函数的订阅,并再次发布了 eventA 事件。这次只有一个回调函数被执行。

如何区分观察者模式与发布订阅模式:

观察者模式和发布订阅模式都是用于处理对象之间的消息传递和通知的模式,它们的区别在于:

  1. 模式结构:观察者模式通常是一个主题对象和一组观察者对象之间的一对多的关系,主题对象维护了观察者列表,并在状态变化时通知所有观察者。而发布订阅模式通常是一个消息中心和多个订阅者之间的一对多的关系,订阅者向消息中心订阅特定类型的消息,当消息中心收到消息时,会将消息分发给所有订阅了该类型消息的订阅者。
  2. 代码实现:观察者模式通常是在主题对象中定义通知方法和观察者列表,观察者对象实现观察者接口,并在主题对象中注册自己。在主题对象状态变化时,会调用观察者的通知方法。而发布订阅模式通常是通过一个消息中心来实现,订阅者向消息中心注册消息处理函数,当消息中心接收到消息时,会调用所有订阅了该消息类型的处理函数。
  3. 耦合度:观察者模式中,主题对象和观察者对象通常是紧密耦合的。观察者对象需要了解主题对象的接口和实现细节。而发布订阅模式中,消息中心和订阅者对象之间通常是松散耦合的。订阅者对象只需要了解消息中心的接口,而不需要了解其他订阅者或消息发送者的实现细节。

总之,观察者模式和发布订阅模式都是用于处理对象之间的消息传递和通知的模式,它们之间的选择应该基于具体的需求和场景。如果需要紧密耦合的对象之间进行通信,例如 MVC 模式中的视图和模型之间的通信,可以选择观察者模式。而如果需要松散耦合的对象之间进行通信,例如系统中不同模块之间的通信,可以选择发布订阅模式。在实践中,两种模式也可以结合使用,例如可以将观察者模式作为发布订阅模式的一种实现方式,使用消息中心来维护观察者列表并向观察者对象发送通知消息。

模板方法模式

模板方法模式(Template Method Pattern):定义一个算法的框架,而将一些步骤延迟到子类中实现。常用于解决算法的复用问题。

模板方法模式是一种行为型设计模式,它定义了一个操作中的算法框架,而将一些步骤延迟到子类中实现。在 JavaScript 中,模板方法模式通常通过继承和重写方法来实现。以下是一个使用模板方法模式实现的 JavaScript 代码示例:

class AbstractClass {
  // 模板方法,定义了算法的骨架
  templateMethod() {
    this.baseOperation1();
    this.requiredOperation1();
    this.baseOperation2();
    this.hook1();
    this.requiredOperation2();
    this.baseOperation3();
    this.hook2();
  }

  // 基本操作,可以在子类中被重写
  baseOperation1() {
    console.log('AbstractClass::baseOperation1');
  }

  baseOperation2() {
    console.log('AbstractClass::baseOperation2');
  }

  baseOperation3() {
    console.log('AbstractClass::baseOperation3');
  }

  // 钩子方法,可以在子类中被重写
  hook1() {}

  hook2() {}

  // 抽象方法,必须在子类中实现
  requiredOperation1() {
    throw new Error('Abstract method has not been implemented.');
  }

  requiredOperation2() {
    throw new Error('Abstract method has not been implemented.');
  }
}

class ConcreteClass extends AbstractClass {
  // 实现必须的方法
  requiredOperation1() {
    console.log('ConcreteClass::requiredOperation1');
  }

  requiredOperation2() {
    console.log('ConcreteClass::requiredOperation2');
  }
}

// 使用
const concreteClass = new ConcreteClass();
concreteClass.templateMethod();

在这个示例中,AbstractClass 是一个抽象类,它定义了模板方法 templateMethod(),该方法定义了算法的骨架,并调用了一系列基本操作和钩子方法。其中,baseOperation1()baseOperation2() 和 baseOperation3() 是基本操作,它们可以在子类中被重写。hook1() 和 hook2() 是钩子方法,它们可以在子类中被重写,但不是必须的。requiredOperation1() 和 requiredOperation2() 是抽象方法,必须在子类中实现。

ConcreteClass 是一个具体类,它继承了 AbstractClass 并实现了必须的方法。在主程序中,我们创建了一个 ConcreteClass 对象 concreteClass,并调用了其模板方法 templateMethod()。该方法按照 AbstractClass 中定义的算法骨架执行,具体执行的细节由 ConcreteClass 中的方法实现。在本例中,templateMethod() 方法执行了一系列基本操作和抽象方法,其中 requiredOperation1() 和 requiredOperation2() 方法在 ConcreteClass 中被实现。

迭代器模式

迭代器模式(Iterator Pattern):提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。常用于解决对聚合对象的遍历问题。

迭代器模式是一种行为型设计模式,它允许客户端通过一致的方式遍历集合中的元素,而无需了解集合的内部实现。在 JavaScript 中,迭代器模式通常通过实现可迭代协议和迭代器协议来实现。以下是一个使用迭代器模式实现的 JavaScript 代码示例:

class Iterator {
  constructor(collection) {
    this.collection = collection;
    this.index = 0;
  }

  next() {
    if (this.hasNext()) {
      return { value: this.collection[this.index++], done: false };
    } else {
      return { value: undefined, done: true };
    }
  }

  hasNext() {
    return this.index < this.collection.length;
  }
}

class IterableCollection {
  constructor(collection = []) {
    this.collection = collection;
  }

  [Symbol.iterator]() {
    return new Iterator(this.collection);
  }
}

// 使用
const collection = new IterableCollection(['a', 'b', 'c']);
for (const item of collection) {
  console.log(item);
}

在这个示例中,Iterator 类是一个迭代器,它维护了对集合的引用和当前遍历的位置,并实现了 next() 和 hasNext() 方法来遍历集合中的元素。IterableCollection 类是一个可迭代的集合,它实现了可迭代协议,在其对象上定义了一个默认的迭代器方法 [Symbol.iterator](),该方法返回一个迭代器对象。在主程序中,我们创建了一个包含三个元素的 IterableCollection 对象 collection,然后使用 for...of 循环遍历集合中的元素。在每次迭代中,for...of 循环会自动调用 Iterator 中的 next() 方法,直到遍历完所有元素为止。