前端常见设计模式 | 青训营

21 阅读5分钟

1. 外观模式

外观模式(Facade Pattern)是一种结构型设计模式,其主要目的是简化复杂系统的接口并提供一个更高级别的接口以供外部使用。

可以将外观模式想象成一个门面或者外观,类似于房子的门面,它把整个系统隐藏在其背后。对于外部使用者而言,只需要通过门面提供的接口来操作系统,而不需要关心背后的实现细节。

外观模式的一个生动的例子是手机的操作界面。手机的操作界面为用户提供了一个简单易用的接口,可以通过点击屏幕上的图标、按钮来进行操作,但实际上在背后有许多不同的系统组件在协作工作。用户不需要关心这些组件的具体实现,只需要使用操作界面提供的接口即可。

很多我们常用的框架和库基本都遵循了外观设计模式,比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。其实在平时工作中我们也会经常用到外观模式进行开发,只是我们不自知而已。

(1)兼容浏览器事件绑定

let addMyEvent = function (el, ev, fn) {
    if (el.addEventListener) {
        el.addEventListener(ev, fn, false)
    } else if (el.attachEvent) {
        el.attachEvent('on' + ev, fn)
    } else {
        el['on' + ev] = fn
    }
};

(2)封装接口

let myEvent = {
    // ...
    stop: e => {
        e.stopPropagation();
        e.preventDefault();
    }
};

优点

   1.减少系统相互依赖。外观模式可以将客户端和子系统解耦,因为客户端只需要与外观对象交互,而不需要了解底层子系统的实现细节。
   2.提高灵活性。外观模式可以提高系统的灵活性,因为它提供了一个更简单的接口,使得客户端可以更容易地使用系统中的功能。
   3. 提高了安全性。因为外观对象可以控制客户端对子系统的访问,并提供一个安全的接口。

缺点

   1.不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。

2.代理模式

代理模式:是为一个对象提供一个代用品或占位符,以便控制对它的访问。

代理模式的生动理解可以类比为一个人找代理去购物。这个人想购买一些物品,但是由于某些原因无法亲自前往商店购买,于是他找到了一个代理人去帮他购买。

代理模式中的代理对象就像是这个代理人,它可以代替真实对象进行一些操作。

例子1:

   假设当A 在心情好的时候收到花,小明表白成功的几率有60%,而当A 在心情差的时候收到花,小明表白的成功率无限趋近于0。小明跟A 刚刚认识两天,还无法辨别A 什么时候心情好。如果不合时宜地把花送给A,花被直接扔掉的可能性很大,这束花可是小明吃了7 天泡面换来的。但是A 的朋友B 却很了解A,所以小明只管把花交给BB 会监听A 的心情变化,然后选择A 心情好的时候把花转交给A,代码如下:
let Flower = function () {}
    let xiaoming = {
        sendFlower: function (target) {
            let flower = new Flower();
            target.receiveFlower(flower)
        }
    }
    let B = {
        receiveFlower: function (flower) {
            A.listenGoodMood(function () {
                A.receiveFlower(flower)
            })
        }
    }
    let A = {
        receiveFlower: function (flower) {
            console.log('收到花', flower)
        },
        listenGoodMood: function (fn) {
            setTimeout(function () {
                fn()
            }, 1000)
    }
   }
   xiaoming.sendFlower(B);

优点

代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用

代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;

缺点

处理请求速度可能有差别,非直接访问存在开销。

3.工厂模式

  工厂模式(Frontend Factory Pattern)是一种创建对象的设计模式,它可以用于解决对象创建时的复杂度和重复性问题。通过工厂模式,我们可以将对象的创建封装到一个工厂函数中,从而简化对象创建的过程。

假设有一个汽车生产工厂,工厂里有多条生产线,每条生产线都有自己的特定功能,比如制造车身、组装发动机、安装座椅等。当有客户需要购买汽车时,工厂会根据客户的要求,在不同的生产线上生产出不同的汽车,最终交给客户。

通常来说,工厂模式包含以下几个角色:

工厂函数(Factory):一个用于创建其他对象的函数,它负责创建对象并返回。

抽象接口(Interface):定义了工厂函数创建对象所需的基本结构和属性。

具体实现(Concrete):实现抽象接口定义的基本结构和属性,同时也可以包含其他的自定义属性和方法。

// 定义抽象接口
class Button {
  constructor(text) {
    this.text = text;
  }
  render() {
    console.log(`Rendering ${this.text} button...`);
  }
}
 
// 定义具体实现
class SubmitButton extends Button {
  constructor() {
    super('Submit');
    this.disabled = false;
  }
  render() {
    console.log(`Rendering ${this.text} button...`);
    console.log(`Disabled: ${this.disabled}`);
  }
}
 
// 定义工厂函数
function createButton(type) {
  switch(type) {
    case 'submit':
      return new SubmitButton();
    default:
      return new Button('Default');
  }
}
 
// 创建按钮实例
const myButton = createButton('submit');
myButton.render(); // 输出 "Rendering Submit button... Disabled: false"

场景

如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择 将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式; 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性。 优点

创建对象的过程可能很复杂,但我们只需要关心创建结果。 构造函数和创建者分离, 符合“开闭原则”。 一个调用者想创建一个对象,只要知道其名称就可以了。 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 缺点

添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度。 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度。

4.单例模式

单例模式是一种创建型设计模式

顾名思义,单例模式中Class的实例个数最多为1。当需要一个对象去贯穿整个系统执行某些任务时,单例模式就派上了用场。

想象一下,你正在编写一个游戏,需要一个全局的游戏管理器,它负责处理所有游戏相关的事务,例如:渲染游戏画面、处理用户输入、处理游戏逻辑等等。为了确保只有一个游戏管理器实例,并且在全局范围内能够访问它,你可以使用单例模式来实现这一点

class GameManager {
  constructor() {
    if (GameManager.instance) {
      return GameManager.instance;
    }
    this.score = 0;
    GameManager.instance = this;
  }
  
  static getInstance() {
    return GameManager.instance || new GameManager();
  }
  
  addScore(points) {
    this.score += points;
    console.log(`Score: ${this.score}`);
  }
}
 
// 使用单例
const gameManager1 = new GameManager();
const gameManager2 = GameManager.getInstance();
gameManager1.addScore(10); // Score: 10
gameManager2.addScore(20); // Score: 30
console.log(gameManager1 === gameManager2); // true

实现单例模式需要解决以下几个问题: 如何确定Class只有一个实例? 如何简便的访问Class的唯一实例? Class如何控制实例化的过程? 如何将Class的实例个数限制为1? 我们一般通过实现以下两点来解决上述问题:

隐藏Class的构造函数,避免多次实例化。 通过暴露一个 getInstance() 方法来创建/获取唯一实例。 Javascript中单例模式可以通过以下方式实现:

// 单例构造器
const FooServiceSingleton = (function () {
  // 隐藏的Class的构造函数
  function FooService() {}
 
  // 未初始化的单例对象
  let fooService;
 
  return {
    // 创建/获取单例对象的函数
    getInstance: function () {
      if (!fooService) {
        fooService = new FooService();
      }
      return fooService;
    }
  }
})();

实现的关键点有:

使用 IIFE创建局部作用域并即时执行;IIFE 是 Immediately Invoked Function Expression 的缩写,指的是立即调用的函数表达式。 getInstance() 为一个 闭包 ,使用闭包保存局部作用域中的单例对象并返回。 我们可以验证下单例对象是否创建成功:

const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();
 
console.log(fooService1 === fooService2); // true