【设计模式】23 种设计模式

9/7/2022 design-pattern

# 概述

# 基本概念

  • SOLID:单一职责原则,开闭原则,里氏代换原则,依赖倒置原则,接口隔离原则,合成复用原则,迪米特法则
  • 设计模式分类
    • 创建型:单例模式、工厂模式、建造者模式
    • 结构型:适配器模式、装饰器模式、代理模式
    • 行为型:策略模式、观察者模式(发布订阅模式)、职责链模式、中介者模式

# 单例模式

  • 实现原理:一个类只有一个实例,并提供一个访问它的全局访问点。

  • 代码实现

    class Singleton {
      static _instance = null;
      static getInstance() {
        if (!Singleton._instance) {
          Singleton._instance = new Singleton();
        }
        return Singleton._instance;
      }
    }
    
    const s1 = Singleton.getInstance();
    const s2 = Singleton.getInstance();
    console.log(s1 === s2); // true
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  • 应用场景

    • Vuex 实现了一个全局的 store 来存储应用的所有状态,store 的实现就是单例模式的典型应用。
      • 通过调用 Vue.use 方法,安装 Vuex 插件。
      • Vuex 插件本质上是一个对象,内部实现了一个 install 方法,这个方法在插件安装时被调用,从而把 store 注入到 Vue 实例中。
      • 通过这种方式,可以保证一个 Vue 实例只会被 install 一次 Vuex 插件,所以每个 Vue 实例只会拥有一个全局的 Store。
  • 优点:节约资源,保证访问的一致性。

  • 缺点:扩展性不友好,因为单例模式一般自行实例化,没有接口。

# 工厂模式

  • 实现原理:根据不同的参数,返回不同类的实例。将对象的创建与对象的实现分离。

  • 代码实现(Event 类)

    class Event {
      constructor() {
        this.eventCache = {};
      }
    
      addEventListener(type, callback, once = false) {
        if (this.eventCache[type] === undefined) {
          this.eventCache[type] = [];
        }
        if (typeof callback === 'function') {
          if (!this.eventCache[type].includes(callback)) {
            this.eventCache.push(callback);
          }
          callback.once = once;
          return this;
        } else {
          throw new Error('Callback must be a function!');
        }
      }
    
      removeEventListener(type, callback) {
        let fns = this.eventCache[type];
        if (Array.isArray(fns)) {
          if (callback === undefined) {
            fns.length = 0;
          } else {
            let idx = fns.indexOf(callback);
            if (idx !== -1) {
              fns.splice(idx, 1);
            }
          }
        }
        return this;
      }
    
      addOnce(type, callback) {
        this.addEventListener(type, callback, true);
      }
    
      removeAll() {
        this.eventCache = {};
      }
    
      dispatch(type, eventData = {}, _this = this) {
        this.eventCache[type].forEach((fn) => {
          fn.call(_this, eventData);
          if (fn.once) {
            this.removeEventListener(type, fn);
          }
        });
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
  • 应用场景

    • document.createElement 创建 DOM 元素,便是典型的工厂模式
      • 方法内部很复杂,但外部使用很简单。只需要传递标签名,这个方法就会返回对应的 DOM 元素
  • 优点

    • 良好的封装,访问者无需了解创建过程,代码结构清晰
    • 扩展性良好,通过工厂方法隔离了用户和创建流程,符合开闭原则
    • 解耦了高层逻辑和底层产品类,符合最少知识原则,不需要的就不要去交流
  • 缺点

    • 给系统增加了抽象性,带来了额外的系统复杂度,不能滥用(合理抽象能提高系统维护性,但可能会提高阅读难度)

# 适配器模式

  • 实现原理:用于解决兼容问题,接口、方法、数据不兼容,将其转换成访问者期望的格式进行使用

  • 代码实现(获取不同格式的数据)

    let data1 = {
      book_id: 1001
      status: 0,
      create: '2021-12-12 08:10:20',
      update: '2022-01-15 09:00:00',
    };
    
    let data2 = {
      id: 1002
      status: 0,
      createTime: 16782738393022,
      updateAt: '2022-01-15 09:00:00',
    };
    
    let data3 = {
      book_id: 1003
      status: 0,
      createTime: 16782738393022,
      updateAt: 16782738393022,
    };
    
    interface bookData {
      book_id: number;
      status: number;
      createAt: string;
      updateAt: string;
    }
    
    interface bookDataType1 {
      book_id: number;
      status: number;
      create: string;
      update: string;
    }
    
    interface bookDataType2 {
      id: number;
      status: number;
      createTime: number;
      updateAt: string;
    }
    
    interface bookDataType3 {
      book_id: number;
      status: number;
      createTime: number;
      updateAt: number;
    }
    
    const getTimeStamp = function (str: string): number {
      return timeStamp;
    };
    
    export const bookDataAdapter = {
      adapterType1(list: bookDataType1[]) {
        const bookDataList: bookData[] = list.map((item) => {
          return {
            book_id: item.book_id,
            status: item.status,
            createAt: getTimeStamp(item.create),
            updateAt: getTimeStamp(item.update),
          };
        });
        return bookDataList;
      },
    
      adapterType2(list: bookDataType2[]) {
        const bookDataList: bookData[] = list.map((item) => {
          return {
            book_id: item.id,
            status: item.status,
            createAt: item.createTime,
            updateAt: getTimeStamp(item.updateAt),
          };
        });
        return bookDataList;
      },
    
      adapterType3(list: bookDataType3[]) {
        const bookDataList: bookData[] = list.map((item) => {
          return {
            book_id: item.book_id,
            status: item.status,
            createAt: item.createTime,
            updateAt: item.updateAt,
          };
        });
        return bookDataList;
      },
    };
    
    const bookDataList = [
      ...bookDataAdapter.adapterType1(type1List),
      ...bookDataAdapter.adapterType2(type2List),
      ...bookDataAdapter.adapterType3(type3List),
    ];
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
  • 应用场景

    • 想要使用一个已经存在的对象,但是接口不满足需求,那么可以使用适配器模式转换成你需要的接口
    • 想要创建一个可以复用的对象,而且确定需要和一些不兼容的对象一起工作,这种情况可以使用适配器模式
    • axios 既适配了浏览器的 XMLHttpRequest,又适配了 Nodejs 的 http 模块
  • 优点:可以使原有逻辑得到更好的复用,有助于避免大规模改写现有代码

  • 缺点:会让系统变得零乱,明明调用 A,却被适配到了 B,如果滥用,那么对可阅读性不太友好

# 代理模式

  • 实现原理:为一个对象提供一个代用品或占位符,以便控制对它的访问

    • 在 ES6 中,使用 Proxy 构建函数能够轻松应用代理模式:const proxy = new Proxy(target, handler);
    • 常见的 js 代理模式
      • 缓存代理:为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果
      • 虚拟代理:把一些开销很大的对象,延迟到真正需要它的时候才去创建,如图片预加载功能
  • 代码实现

    // 缓存代理:实现一个求乘积的函数
    var multi = function () {
      var a = 1;
      for (var i = 0; i < arguments.length; i++) {
        a = a * arguments[i];
      }
      return a;
    };
    
    var proxyMult = (function () {
      var cache = {};
      return function () {
        var args = Array.prototype.join.call(arguments, ',');
        if (args in cache) {
          return cache[args];
        } else {
          cache[args] = multi.apply(this, arguments);
          return cache[args];
        }
      };
    })();
    
    proxyMult(1, 2, 3, 4);
    proxyMult(2, 3, 4, 5);
    proxyMult(1, 2, 3, 4);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 虚拟代理:实现图片预加载
    let myImage = (function () {
      let imgNode = document.createElement('img');
      document.body.appendChild(imgNode);
      return {
        setSrc: function (src) {
          imgNode.src = src;
        },
      };
    })();
    
    let proxyImage = (function () {
      let img = new Image();
      img.onload = function () {
        myImage.setSrc(this.src);
      };
      return {
        setSrc: function (src) {
          myImage.setSrc(
            'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif'
          );
          img.src = src;
        },
      };
    })();
    
    proxyImage.setSrc('https://xxx.jpg');
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
  • 应用场景

    • 很多前端框架或者状态管理框架都使用代理模式,用于监听变量的变化
    • 使用代理模式代理对象的访问的方式,一般又被称为拦截器,如 axios 的 interceptor 可以提前对服务器返回的数据进行一些预处理

# 策略模式

  • 实现原理:定义一系列算法,根据输入的参数决定使用哪个算法(算法的实现和算法的使用分开)

    • Context:封装上下文,按需调用策略,屏蔽外界对策略的直接调用,只对外提供一个接口
    • Strategy:含有具体的算法,其方法的外观相同
    • StrategyMap:所有策略的合集,供封装上下文调用
  • 代码实现

    function priceCalculate(discountType, price) {
      if (discountType === 'discount200-20') {
        return price - Math.floor(price / 200) * 20;
      } else if (discountType === 'discount300-50') {
        return price - Math.floor(price / 300) * 50;
      } else if (userType === 'discount500-100') {
        return price - Math.floor(price / 500) * 100;
      }
    }
    
    // 1. 随着折扣类型的增加,if-else 会变得越来越臃肿
    // 2. 折扣活动算法改变或折扣类型增加时,都需要改动 priceCalculate 方法,违反开闭原则
    // 3. 复用性差,如果其他地方有类似的算法,但规则不一样,上述代码不能复用
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const discountMap = {
      'discount200-20': function (price) {
        return price - Math.floor(price / 200) * 20;
      },
      'discount300-50': function (price) {
        return price - Math.floor(price / 300) * 50;
      },
      'discount500-100': function (price) {
        return price - Math.floor(price / 500) * 100;
      },
    };
    
    function priceCalculate(discountType, price) {
      return discountMap[discountType] && discountMap[discountType](price);
    }
    
    // 将算法的实现和算法的使用分开,但抽象程度并不高
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const priceCalculate = (function () {
      const discountMap = {
        'discount200-20': function (price) {
          return price - Math.floor(price / 200) * 20;
        },
        'discount300-50': function (price) {
          return price - Math.floor(price / 300) * 50;
        },
        'discount500-100': function (price) {
          return price - Math.floor(price / 500) * 100;
        },
      };
      return {
        addStategy(stategyName, fn) {
          if (discountMap[stategyName]) return;
          discountMap[stategyName] = fn;
        },
        priceCal(discountType, price) {
          return discountMap[discountType] && discountMap[discountType](price);
        },
      };
    })();
    
    priceCalculate.priceCal('discount200-20', 250);
    priceCalculate.addStategy('discount800-200', function (price) {
      return price - Math.floor(price / 800) * 200;
    });
    
    // 隐藏算法的实现:借助 IIFE 使用闭包的方式,提供一个添加策略的方法
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
  • 应用场景

    • 多个算法只有行为上有些不同,可以考虑策略模式动态选择算法;需要多重判断,可以考虑策略模式规避多重条件判断。
    • 使用 Redux 时 action 的分派。
  • 优点:策略相互独立,可以互相切换,提高了灵活性、复用性以及可维护性;可扩展性好,满足开闭原则。

  • 缺点

    • 策略相互独立,一些复杂的算法逻辑无法共享,造成资源浪费。
    • 用户在使用策略时,需要了解具体的策略实现(不满足最少知识原则,增加了使用成本)。

# 观察者模式

  • 实现原理:将多个 observer(观察者) 注册到某个对象(被观察者)上,当该对象发生变更时,便自动通知其上的所有 observer 进行更新。

  • 代码实现

    let observerIds = 0;
    
    class Subject {
      constructor() {
        this.observers = [];
      }
    
      addObserver(observer) {
        this.observers.push(observer);
      }
    
      removeObserver(observer) {
        this.observers = this.observers.filter((obs) => {
          return obs.id !== observer.id;
        });
      }
    
      notify() {
        this.observers.forEach((observer) => observer.update(this));
      }
    }
    
    class Observer {
      constructor() {
        this.id = observerIds++;
      }
    
      update(subject) {
        // Todo: update something
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
  • 应用场景

    • Vue 双向绑定中的发布订阅模式
      • 简述:响应式数据相当于被观察者;组件相当于观察者;响应式数据发生变更会通知相关组件进行视图更新。
      • Vue 通过 defineProperty 或者 proxy 劫持各数据的 setter 和 getter,并为每个数据添加一个订阅者列表,这个列表将会记录所有依赖这个数据的组件,响应式后的数据相当于消息的发布者。
      • 每个组件都对应一个 Watcher 订阅者,当组件渲染函数执行时,会将本组件的 Watcher 加入到所依赖的响应式数据的订阅者列表中,相当于完成了一次订阅,这个过程叫做依赖收集。
      • 当响应式数据发生变化时,会触发 setter,setter 负责通知数据的订阅者列表中的 Watcher,Watcher 触发组件重新渲染来更新视图。
    • EventEmitter
  • 优点:目标变化就会通知观察者

    • 时间解耦:注册的订阅行为由发布者决定何时调用,订阅者无需持续关注,由发布者负责通知
    • 对象解耦:发布者无需知道消息的接受者,只需遍历订阅该消息类型的订阅者发送消息,解耦了发布者和订阅者之间的联系
  • 缺点:目标和观察者耦合度高,必须同时引入被观察者和观察者才能达到响应式的效果

    • 资源消耗:创建订阅者需要一定的时间和内存
    • 增加复杂度:弱化了联系,难以维护调用关系,增加了理解成本

# 参考

Last Updated: 10/8/2022, 2:29:52 PM