设计模式之-代理模式&单例模式&策略模式

代理模式

目的

当调用方不方便直接操作某个对象,又或者希望在访问对象之前或之后做某些操作时,可以使用代理模式。

  • 使用一个代理对象作为本体对象的替身;
  • 调用方访问代理对象,代理对象做完某些操作后,按照调用方的需求去访问本体对象,把结果返回给调用方;

问题 & 解决办法

举个例子,我们有一个存放着超多数据的对象,如果把这个对象直接在程序中初始化的话,肯定会占据很多内存;而且,程序不一定一开始就需要用到这个对象,也不一定需要这个对象的所有数据;所以,比较经济的方法是,把这个超大对象放在服务端,当程序需要的时候再发起网络请求去访问。

问题是,在程序中可能不止一处要用到这个对象,我们总不想把发起网络请求的逻辑到处复制粘贴吧。而代理模式的思想就是,提供一个代理对象(替身),把这个代理对象伪装成本体对象,然后把本体对象藏起来。调用方需要数据的时候,就去访问代理对象,其实调用方是分不清自己在访问代理对象还是本体对象的,它也根本不需要区分。

用图解释大概是这个样子:

应用

代理模式大概有以下几种用途:

  • 惰性初始化:在真正需要的时候才通过代理对象去初始化本体对象,一般是本体对象消耗很大的时候才需要这样做;

  • 控制访问权限:代理对象负责甄别哪些调用方有权访问本体对象,对有权访问的调用方,代理对象把它们的请求转发给本体对象,对于无权访问的,就无视它们的请求;

  • 打印日志:想要记录某个对象的访问日志时,可以用一个代理对象来做这件事,这样就不需要对本体对象做修改;

  • 缓存请求结果:就像上例一样,代理对象负责发送网络请求并缓存请求结果,下次有同样的请求时可以直接返回缓存的数据;

  • 智能引用:可以用一个代理对象来记录某个对象当前有多少个引用,如果没有调用方引用这个本体对象,这个对象的内存就可以被及时释放了;

优点

  • 可以通过代理对象来控制本体对象而不必修改调用方代码和本体对象;
  • 可以很方便地引入多个代理对象,代理对象们可以链式访问(对代理对象来说,访问另一个代理对象和访问本体对象没什么不同);

唯一的修改是,把调用方的访问对象从本体对象改成代理对象,其他不变,因为代理对象是一个“伪本体对象”,在调用方看来,它和本体对象是一样的。

单例模式

单例模式就是一个 class 永远都返回同一个实例,而且这个实例还可以在全局中访问到。

听起来是不是很像一个全局变量?

单例模式 vs 全局变量

  • 同样都是全局可访问,全局变量有被其他代码修改的风险,而单例模式提供的实例不能通过外部代码来修改替换,除非这个 class 暴露了修改实例的方法。

  • 单例模式还能封装一些代码逻辑。

实现思路

单例模式永远返回同一个实例对象,所以我们不能使用普通的构造函数来实现,因为每次 new 的时候都会创建一个新的实例。

其实如果是 JS 的普通构造函数语法的话,new 的时候如果函数中返回一个对象,那 new 出来的对象就会被丢弃,然后我们也可以把单例对象作为构造函数的静态属性缓存起来,不过这样我们也把单例对象暴露出去了。

我们得把构造函数和单例对象隐藏起来,然后另外暴露一个获取单例对象的方法。在这个方法中,我们需要实现的是:

  1. 首次调用时,在方法中调用隐藏的构造函数,创建实例对象并缓存起来;
  2. 之后的调用就直接返回缓存的实例对象;

实现代码

因为 JS 的 class 还不支持私有属性,所以我们先用一个 IIFE 来实现单例模式。

let count = 0

const Singleton = (function () {
  // 缓存单例实例对象
  let instance = null

  // 只有首次调用 getInstance 方法时才会用 new 调用 Constructor 方法
  // 外部代码无法通过 new 调用 Constructor 创建实例对象
  // Contructor 只会执行一次
  function Constructor() {
    count++
  }

  function getInstance() {
    // 首次调用,创建单例实例并缓存
    if (!instance) {
      instance = new Constructor()
    }
    // 之后调用,直接返回缓存的实例对象
    return instance
  }
  // 暴露获取单例对象的方法
  return {
    getInstance
  }
}())

const a = Singleton.getInstance()
const b = Singleton.getInstance()
console.log(a === b); // true
console.log(count); // 1

优点

  • 保证了一个 class 只有一个实例;
  • 这个实例是全局可访问的;
  • 惰性初始化,单例对象只有在首次获取的时候才会被创建,这也是和全局变量不同的一个点。
策略模式

问题

比如要压缩一张图片,可选择的压缩算法有 jpgpnggif 等等。我们实现了一个 ImageCompressor class,提供了 compress 方法来处理这个任务,代码如下。

class ImageCompressor {
  constructor() {}
  compress(img, algorithm) {
    let compressedImg = null
    switch (algorithm) {
      case 'jpg':
        // jpg 的压缩算法
        // compressedImg = ...
        break
      case 'png':
        // png 的压缩算法
        break
      case 'gif':
        // gif 的压缩算法
        break
      default:
        break
    }
    return compressedImg
  }
}

const imageCompressor = new ImageCompressor()
const pngImg = imageCompressor('exampleImageFile', 'png')
const jpgImg = imageCompressor('exampleImageFile', 'jpg')

可以看到在上面的代码中,所有压缩算法都是在 compress 方法中实现的,而在实际中,每种算法的逻辑都十分复杂,如果都写在同一个方法中,这个方法很快就会膨胀了。这种写法导致的问题就是,如果之后要修改或者新增某个算法,都很有可能会不小心影响到其他代码。

为了代码更容易维护,我们需要把这些算法拆分成独立的小块,并通过一个“代言人”来“管理”它们。

用“代言人”和“管理”好像不那么准确,但我又想不到什么词了,意会意会。

什么时候使用策略模式

从以上代码中可以观察到,compress 的输入和输出是差不多的:

  • 输入一张图片
  • 输出一张压缩后的图片

而不同的是:

  • 压缩处理的方法

遇到这种模式的问题我们都可以使用策略模式来解决:

  • 把不同的 处理方法 抽离成各自独立的 class
  • 每一个 处理方法 就是一个解决问题的 策略
  • 再通过一个 Context class 来提供统一的对外接口,Context 内部再调用不同的 策略 方法

一个简单例子

假设要写一个可以同时处理两数加减乘除的函数,我们可能会实现成以下的样子:

doMath 函数接收两个操作数 ab,以及一个算术类型 operation 作为参数,然后根据不同的算术类型返回不同的数学计算结果。

const doMath = (a, b, operation) => {
  switch (operation) {
    case 'ADD':
      return a + b
    case 'MINUS':
      return a - b
    case 'MULTIPLY':
      return a * b
    case 'DIVIDE':
      return a / b
    default:
      return
  }
}

doMath(1, 2, 'ADD') // 3
doMath(1, 2, 'MINUS') // -1
doMath(1, 2, 'MULTIPLY') // 2
doMath(1, 2, 'DIVIDE') // 0.5

可以观察到,多次调用 doMath 函数的共同点在于:

  • 都是输入两个数字
  • 得到这两个数字经过某种计算后的结果作为返回值

而不同点就在于:

  • 具体的计算方式是不一样的

这个问题模式就很适合使用策略模式来解决。

套用策略模式

用 OOP 的形式来实现的话,我们先把上面的例子改写成 class 的形式吧,改写方式之一:

class SimpleMath {
  constructor() {
    this.operations = {
      ['ADD']: (a, b) => a + b,
      ['MINUS']: (a, b) => a - b,
      ['MULTIPLY']: (a, b) => a * b,
      ['DIVIDE']: (a, b) => a / b,
    }
  }
  calculate(a, b, operation) {
    return this.operations[operation](a, b)
  }
}

目前所有算法都是在 SimpleMath 中实现的,接下来我们尝试把每个算法抽离成独立的 class:

// 不同算法被抽离成不同的 class
// 实现的效果是这些 class 的都应该有相同的实例方法,但这些实例方法做的事情不一样
// p.s. 按理说这些 class 都应该实现同样的 interface,但 JS 中并没有 interface
class Add {
  calculate(a, b) {
    return a + b
  }
}
class Minus {
  calculate(a, b) {
    return a - b
  }
}
class Multiply {
  calculate(a, b) {
    return a * b
  }
}
class Divide {
  calculate(a, b) {
    return a / b
  }
}

// 原本的 SimpleMath 就只保留一个对具体算法实例的引用 operation
// SimpleMath 并不关心具体使用什么算法,调用方在调用 SimpleMath 的时候把具体的算法对象传过来,SimpleMath 负责调用这个算法对象的 calculate 方法
class SimpleMath {
  constructor(operation) {
    // operation 保存着具体算法实例
    this.operation = operation
  }
  calculate(a, b) {
    // SimpleMath 的 calculate 方法只是负责调用具体算法实例的 calculate 方法
    return this.operation.calculate(a, b)
  }
}

// 调用方代码:
// 调用方必须知道自己需要的是哪种算法,并把对应的算法实例传给 SimpleMath
const add = new SimpleMath(new Add())
add.calculate(1, 2) // 3
const multiply = new SimpleMath(new Multiply())
multiply(1, 2) // 2

策略模式的构成

问题

  • 策略模式中有几个角色?
  • 每个角色分别负责什么?
  • 各个角色之间是如何联系的?

回答

策略模式中有 3 个角色:

  • Client: 调用方,是有某项任务需要完成的角色,可以有很多个;
  • Context: 比如上方的 SimpleMath,是负责完成某项任务的角色,但只知道需要完成一项任务,并不知道任务具体应该如何完成,只有一个;
  • Strategy: 完成任务的具体方法,可以有很多个;

Client 通过调用 Context 并指定某个 Strategy 来间接调用相应 Strategy 的某个方法。

其实不用 class 来实现,只要是符合这种思想的都是策略模式吧,把 strategy 拆分成函数也行吧,个人看法个人看法。

🌟github仓库地址

原文链接:juejin.im

上一篇:ts+react 结合的问题
下一篇:转行学前端的第 44 天 : 了解 ECMAScript 数据类型转换(二)

相关推荐

  • 🦄️ Web 站点暗色模式探索

    本文存在一些DEMO,适合在 PC 端阅览 最近发布了自己的新博客 xlbd.me , 博客站点设计了暗色模式风格,但是当时只是基于媒体查询 perfers-color-schema 实现的跟随系...

    1 个月前
  • 🔥 Promise|async|Generator 实现&原理大解析 | 9k字

    笔者刚接触async/await时,就被其暂停执行的特性吸引了,心想在没有原生API支持的情况下,await居然能挂起当前方法,实现暂停执行,我感到十分好奇。好奇心驱使我一层一层剥开有关JS异步编程的...

    7 个月前
  • 默认值选项的JavaScript设计模式?

    默认值选项的JavaScript设计模式? ...

    3 年前
  • 高阶函数&&高阶组件

    高阶函数 特点: 接受函数类型的参数。 返回值是函数。 高阶函数具有可扩展性。 常见的高阶函数: 定时器 setTimeout(); setInterval() Promise(); 数组相关:...

    8 个月前
  • 高级 Angular 组件模式 (3b)

    03-b Enhance Components with Directives 原文: Enhance Components with Directives Kent C. Dodds的第四篇文章...

    3 年前
  • 高级 Angular 组件模式 (3a)

    03-a Communicate Between Components Using Dependency Injection 原文: Communicate Between Components U...

    3 年前
  • 高效的Mobx模式(Part 3 高阶应用实例)

    前两部分侧重于MobX的基本构建块。 有了这些块,我们现在可以通过MobX的角度开始解决一些真实场景。 这篇文章将是一系列应用我们迄今为止所见概念的例子。 当然,这不是一个详尽的清单,但应该让你体会到...

    2 年前
  • 高效的Mobx模式 - (Part 1)

    起因 很早之前看到的一篇关于mobx的文章,之前记得是有人翻译过的,但是怎么找都找不到,故花了点时间通过自己那半桶水的英文水平,加上Google翻译一下,对于初学者,以及mobx的开发者提供些许帮助。

    2 年前
  • 面对对象严格模式

    设计目的 启用方法 显式报错 只读属性不可写 只设置了取值器的属性不可写 禁止扩展的对象不可扩展 eval、arguments 不可用作标识名 函数不能有重名的参数 禁止八进制的前缀0表示法 增强的安...

    1 年前
  • 面向对象的JavaScript构造函数模式:新古典与原型

    Cheeso提出了一个问题:OO Javascript constructor pattern: neo-classical vs prototypal,或许与您遇到的问题类似。

    3 年前

官方社区

扫码加入 JavaScript 社区