设计模式

2022/9/10

# 概念

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。类似于游戏攻略,使用设计模式是为了重用代码,让代码更容易被人理解、保证代码的可靠性。

# 原则

  1. 开闭原则: 对扩展开放,对修改关闭
  2. 里氏代换原则: 任何基类出现的地方,子类一定可以出现
  3. 依赖倒转原则: 针对接口编程,依赖于抽象而不依赖于具体
  4. 接口隔离原则: 使用多个隔离的接口,比使用单个接口要好他,即要降低类之间的耦合度
  5. 迪米特法则(最少知道原则): 一个实体应当尽量少地和其他实体之间发生相互作用,使得系统功能模块相对独立
  6. 合成复用原则: 尽量使用合成/聚合的方式,而不是使用继承

# 类别

# 创建型模式

描述: 提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是直接使用 new 操作符
包括:

  • 工厂模式
  • 抽象工厂模式
  • 单例模式
  • 建造者模式
  • 原型模式

# 结构性模式

描述: 关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式
包括:

  • 适配器模式
  • 桥接模式
  • 过滤器模式
  • 组合模式
  • 装饰器模式
  • 外观模式
  • 享元模式
  • 代理模式

# 行为型模式

描述: 关注对象之间的通信
包括:

  • 责任链模式
  • 命令模式
  • 解释器模式
  • 迭代器模式
  • 中介者模式
  • 备忘录模式
  • 观察者模式
  • 发布订阅模式
  • 状态模式
  • 空对象模式
  • 策略模式
  • 模板模式
  • 访问者模式

# J2EE 模式

描述: 特别关注表示层
包括:

  • MVC 模式
  • 业务代表模式
  • 组合实体模式
  • 数据访问模式
  • 前端控制器模式
  • 拦截过滤器模式
  • 服务定位器模式
  • 传输对象模式

# 前端常用设计模式

# 工厂模式

工厂模式提供了一种集中化、统一化的方式,避免了分散创建对象导致的代码重复、灵活性差的问题。当构造函数过多,且创建的对象之间存在某些关联(有同一个父类、实现同一个接口等),往往可以使用工厂模式。

以上图汽车的生产为例:

// 汽车构造函数
function SuzukiCar(color) {
  this.color = color;
  this.brand = 'Suzuki';
}

// 汽车构造函数
function HondaCar(color) {
  this.color = color;
  this.brand = 'Honda';
}

// 汽车构造函数
function BMWCar(color) {
  this.color = color;
  this.brand = 'BMW';
}

// 汽车品牌枚举
const BRANDS = {
  suzuki: 1,
  honda: 2,
  bmw: 3
}

/**
 * 汽车工厂
 */
function CarFactory() {
  this.create = function (brand, color) {
    switch (brand) {
      case BRANDS.suzuki:
        return new SuzukiCar(color);
      case BRANDS.honda:
        return new HondaCar(color);
      case BRANDS.bmw:
        return new BMWCar(color);
      default:
        break;
    }
  }
}

在创建时只需要传入对应参数就可以创建对应的对象,而不需要去分别调用各自的构造函数

const carFactory = new CarFactory();
const cars = [];

cars.push(carFactory.create(BRANDS.suzuki, 'brown'));
cars.push(carFactory.create(BRANDS.honda, 'grey'));
cars.push(carFactory.create(BRANDS.bmw, 'red'));

function say() {
  console.log(`Hi, I am a ${this.color} ${this.brand} car`);
}

for (const car of cars) {
  say.call(car);
}

/*
输出结果:
Hi, I am a brown Suzuki car
Hi, I am a grey Honda car
Hi, I am a red BMW car
*/

# 单例模式

单例模式即一个类最多只能有一个实例,常用于当需要一个全局对象贯穿整个系统指行某些任务的情况

  // 单例构造器
const FooServiceSingleton = (function () {
  // 隐藏的Class的构造函数
  function FooService() {}

  // 未初始化的单例对象
  let fooService;

  return {
    // 创建/获取单例对象的函数
    getInstance: function () {
      if (!fooService) {
        fooService = new FooService();
      }
      return fooService;
    }
  }
})();

let ob1 = FooServiceSingleton.getInstance();
let ob2 = FooServiceSingleton.getInstance();

console.log(ob1 === ob2)  // true

# 外观模式

外观模式为子系统的中的一组接口提供了一个统一的高层接口,使系统更容易调用,也就是将多个复杂的逻辑进行抽象,整合成一个简单易用的API。
比如添加事件监听,一些添加方法有些浏览器可能不支持,我们就可以使用外观模式,将多种添加方式整合成一个方法,使所有浏览器都能支持。

// 添加事件监听
  function addEvent(element, event, fn){
    if(element.addEventListener){
      element.addEventListener(event, fn, false)
    } else if(element.attachEvent){
      element.attachEvent('on' + event, fn)
    } else {
      element['on' + event] = fn
    }
  }

  // 移除事件监听
  function removeEvent(element, event, fn){
    if(element.removeEventListener){
      element.removeEventListener(event, fn, false)
    } else if(element.detachEvent){
      element.detachEvent('on' + event, fn)
    } else {
      element['on' + event] = null
    }
  }

# 代理模式

当访问一个对象太耗时时或需要增加额外的逻辑又不改变对象的情况下,就可以使用代理模式,如 ES6 中新增的 Proxy (opens new window)

# 观察者模式和发布订阅模式

观察者模式和发布订阅模式类似,区别在于后者在被观察者和观察者之间存在了一个调度中心。

  • 观察者模式
let subject_id = 0
  let observer_id = 0

  class Subject{
    constructor(){
      this.observerList = []
      this.sid = subject_id++;
    }

    // 添加观察者
    add(observer){
      this.observerList.push(observer)
    }

    // 向观察者发送信息
    notify(msg){
      this.observerList.forEach(v => v.getMsg(msg))
    }

    // 移除指定观察者
    del(observer){
      this.observerList = this.observerList.filter(v => {
        return v.oid !== observer.oid
      })
    }
  }

  class Observer{
    constructor(name){
      this.name = name
      this.oid = observer_id++  // 赋予唯一id,方便删除
    }
    getMsg(msg){
      console.log(`${this.name}收到新消息:${msg}`)
    }
  }

  let sub = new Subject();
  let ob1 = new Observer('张三')
  let ob2 = new Observer('李四')

  sub.add(ob1)
  sub.add(ob2)

  sub.notify('谢谢关注')

  sub.del(ob1)
  sub.notify('还在吗')
  • 发布订阅模式
// 发布者
  class Publisher{
    constructor(name){
      this.name = name
      this.deps = []
    }
    addDep(dep){
      this.deps.push(dep);
    }
    publish(dep){
      this.deps.forEach(v => {
        if(v === dep) v.notify(this.name)
      })
    }
  }

  // 订阅者
  class Subscriber{
    constructor(name){
      this.name = name
    }
    getMsg(callback, msg){
      callback(msg, this.name)
    }
  }

  // 调度中心
  class Dep{
    constructor(callback){
      this.subs = []
      this.callback = callback
    }
    addSub(sub){
      this.subs.push(sub)
    }
    notify(msg){
      this.subs.forEach(v => v.getMsg(this.callback, msg))
    }
  }

  function update(pub, sub){
    console.log(`to ${sub}: 您关注的${pub}更新了视频`)
  }

  let dep = new Dep(update);
  let pub = new Publisher('影视飓风')
  let sub1 = new Subscriber('张三')
  let sub2 = new Subscriber('李四')

  pub.addDep(dep) // 发布者添加调度中心

  // 由调度中心添加订阅者,发布者不需要知道谁订阅了
  dep.addSub(sub1) 
  dep.addSub(sub2)

  // 发布者发布信息给调度中心,调度中心就会自动将消息处理后发给所有订阅者
  pub.publish(dep)

# 中介模式

在中介模式中,中介者包装了一系列对象相互作用的方法,使得这些对象不必直接相互作用,而由中介者协调它们之间的交互,降低它们之间的耦合度。当某些对象之间的作用发生改变时,不会立即影响其他一些对象间的作用。
中介者有点类似于观察者,但中介者是处理同级之间的交互,而观察者模式是处理观察者和被观察者之间的交互。
中介者一般可用于实现聊天室

参考资料
前端9种设计模式 (opens new window)