几种常用的设计模式

2023-05-13· 10min

#观察者模式(Observer Pattern)

  • 令多个观察者(Observer)同时监听某一个主题(Subject), 当主题(Subject)发生改变时, 所有的观察者都会收到通知并更新
  • 观察者(Observer)有多个, 被观察的主题(Subject)只有一个

#优缺点

  • 优点
    • 观察者和被观察者之间, 建立了一个抽象耦合,易于扩展观察者和被观察者
    • 支持广播通信
  • 缺点
    • 若观察者过多,会使触发时间过长
    • 若有循环依赖,会导致系统奔溃

#代码实现

class Subject {
  constructor() {
    this.observers = [];
  }

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

  notifyObservers(...args) {
    this.observers.forEach((observer) => observer.getMsg(...args));
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }
  getMsg(...args) {
    console.log(`${this.name} get a msg: ${args}`);
  }
}

var obs1 = new Observer("obs1");
var obs2 = new Observer("obs2");

var sub = new Subject();
sub.addObserver(obs1, obs2);

sub.notifyObservers(`hi~ ${+new Date()}`);
// obs1 get a msg: hi~ 1684207960180
// obs2 get a msg: hi~ 1684207960180

#发布-订阅模式(Publish–Subscribe pattern)

  • 是一种消息范式,发布者(Publisher)和订阅者(Subscriber)不用互相知道,发布者通过广播通道(Event Channel)将消息广播出去,订阅者根据自己的订阅得到相应的消息。
  • 是一种对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。
  • 是一种经过解耦合的观察者模式。
  • 和观察者模式的区别:
    • 观察者模式:观察者(Observer)知道 主题(Subject) ,主题(Subject)一直保持对观察者进行记录。应用场景多数是同步
    • 发布订阅模式:发布者(Publisher)和订阅者(Subscriber)不知道对方的存在,它们只能通过 广播通道 进行通信。应用场景多数是异步

#优缺点

  • 优点
    • 对象之间解耦
    • 异步编程中,代码可以松耦合
  • 缺点
    • 弱化对象之间的联系,程序难以追踪维护

#代码实现

class PubSub {
  constructor() {
    this.subscribes = new Map();
  }

  addSubEv(type, callback, once) {
    const sub = this.subscribes.get(type) || [];
    sub.push({ fn: callback, once });
    this.subscribes.set(type, sub);
  }

  // 订阅
  sub(type, callback) {
    this.addSubEv(type, callback);
  }

  // 订阅一次
  subOnce(type, callback) {
    this.addSubEv(type, callback, true);
  }

  // 取消订阅
  subCancel(type, callback) {
    const sub = this.subscribes.get(type);
    if (sub) {
      this.subscribes.set(
        type,
        sub.filter(({ fn }) => fn !== callback),
      );
    }
  }

  // 发布
  pub(type, ...args) {
    const sub = this.subscribes.get(type) || [];
    sub.forEach(({ fn }) => fn.call(this, ...args));

    const newSub = sub.filter((item) => !item.once);
    this.subscribes.set(type, newSub);
  }
}
// PS: use var for repeat test with devtool console...
var pubSub = new PubSub();
var EV_TYPE_MAP = {
  ev1: 1,
  ev2: 2,
};

pubSub.sub(EV_TYPE_MAP.ev1, () => {
  console.log("ev1 console");
});
pubSub.sub(EV_TYPE_MAP.ev1, () => {
  console.log("ev1 again console");
});
pubSub.pub(EV_TYPE_MAP.ev1); // ev1 console、ev1 again console

pubSub.subCancel(EV_TYPE_MAP.ev1, () => {
  console.log("ev1 subCancel console");
});

pubSub.subOnce(EV_TYPE_MAP.ev2, () => {
  console.log("ev2 console");
});
pubSub.pub(EV_TYPE_MAP.ev2); // ev2 console...

pubSub.pub(EV_TYPE_MAP.ev2); // nothing

#策略模式(Strategy Pattern)

  • 定义一系列算法,并将每种算法分别放入独立的类中,以使算法的对象能够相互替换

#优缺点

  • 优点
    • 对开闭原则的完美支持
  • 缺点
    • 策略类会增多,客户端必须知道所有的策略类,并理解其区别

#代码实现

var strategy = function (st) {
  switch (st) {
    case "S1":
      return 1;
    case "S2":
      return 2;
    default:
      break;
  }
};
console.log(strategy("S1"));

#单例模式(Singleton Pattern)

  • 确保一个类只有一个实例,并提供一个全局访问点以获取该实例

#优缺点

  • 优点
    • 提供全局访问点,方便访问
    • 避免全局变量污染
    • 避免重复创建,减少内存占用
  • 缺点
    • 没有接口,无法继承
    • 违反开闭原则,若类的功能变化,需要修改代码
    • 全局状态的滥用会导致代码难以理解和维护
    • 会隐藏类之间的依赖关系,难以测试

#场景

  • 全局配置、缓存、模态框等(在全局需要共享/复用的场景)
  • 全局状态管理(Vuex、Redux...),全局对象(Jquery的$、浏览器中的window、document等)、ES6的模块导出等

#代码实现

// 通过 IIFE 和 闭包,将SingleClass封装在模块内部,外部无法直接访问,确保了单例的安全性
const SingleDialog = (function () {
  let instance;

  class SingleClass {
    constructor(name) {
      this.name = name;
    }

    logName() {
      console.log(`This is SingleDialog: ${this.name}`);
    }
  }

  return {
    getInstance(name) {
      if (!instance) {
        instance = new SingleClass(name);
      }
      return instance;
    },
  };
})();

const single1 = SingleDialog.getInstance("Login");
const single2 = SingleDialog.getInstance("Home");
console.log(`single1 === single2: ${single1 === single2}`); // true
single1.logName(); // Login
single2.logName(); // Login
class SingleDialog {
  // 静态属性保存唯一实例
  private static instance: SingleDialog;
  private name: string;

  // 私有构造函数,防止外部实例化
  private constructor(name: string) {
    this.name = name;
  }

  // 静态方法获取唯一实例
  public static getInstance(name: string): SingleDialog {
    if (!SingleDialog.instance) {
      SingleDialog.instance = new SingleDialog(name);
    }
    return SingleDialog.instance;
  }

  public logName(): void {
    console.log(`This is SingleDialog name: ${this.name}`);
  }
}

const single1 = SingleDialog.getInstance("Login");
const single2 = SingleDialog.getInstance("Home");
console.log(`single1 === single2: ${single1 === single2}`); // true
single1.logName(); // Login
single2.logName(); // Login