美文网首页
Node.js 设计模式笔记 —— Proxy 模式

Node.js 设计模式笔记 —— Proxy 模式

作者: rollingstarky | 来源:发表于2022-05-13 00:08 被阅读0次

    代理(proxy) 可以理解为一种对象,其能够控制客户端对另一个对象(subject)的访问。代理(proxy)和目标对象(subject)拥有完全相同的接口,可以自由地进行替换。
    proxy 会拦截所有或者部分本应该直接交给 subject 执行的操作,通过额外的预处理或后处理增强其行为,再转发给 subject。

    Proxy pattern schematic

    Proxy 的主要应用场景:

    • Data validation:proxy 对输入数据进行验证,再转发给 subject
    • Security:proxy 检查客户端是否有权限执行请求的操作,若检查通过则将请求转发给 subject
    • Caching:proxy 负责维护一份内部缓存,只有当请求的数据不在缓存中时,才将该请求转发给 subject 处理
    • Lazy initialization:若创建某个对象代价很高,proxy 可以延迟该创建操作直到必要的时候
    • Logging:proxy 拦截函数和对应的参数,在函数执行的同时记录日志信息
    • Remote objects:proxy 可以接收一个远程对象并令其表现为本地对象

    示例代码:StackCalculator

    class StackCalculator {
      constructor() {
        this.stack = []
      }
    
      putValue(value) {
        this.stack.push(value)
      }
    
      getValue() {
        return this.stack.pop()
      }
    
      peekValue() {
        return this.stack[this.stack.length - 1]
      }
    
      clear() {
        this.stack = []
      }
    
      divide() {
        const divisor = this.getValue()
        const dividend = this.getValue()
        const result = dividend / divisor
        this.putValue(result)
        return result
      }
    
      multiply() {
        const multiplicand = this.getValue()
        const multiplier = this.getValue()
        const result = multiplier * multiplicand
        this.putValue(result)
        return result
      }
    }
    
    
    const calculator = new StackCalculator()
    calculator.putValue(3)
    calculator.putValue(2)
    console.log(calculator.multiply())  // 3 * 2 = 6
    calculator.putValue(2)
    console.log(calculator.multiply())  // 6 * 2 = 12
    

    现代的计算器基本上都遵循类似的逻辑,即上一个式子的计算结果可以作为下一次计算的输入。
    在 JavaScript 中,当用户尝试除以 0 时,并不会报错而是返回 Infinity。现在我们尝试借助 Proxy 模式来增强 StackCalculator 除以 0 时的行为。

    Object composition

    组合(Composition)表示一个对象通过引用另一个对象,来扩展或者使用后者的功能。
    借助组合可以实现 Proxy 模式。创建一个新的对象,令其有着和 subject 完全一致的接口,同时内部还保存着一个对 subject 的引用。参考如下代码:

    class StackCalculator {
      // see above
    }
    
    class SafeCalculator {
      constructor(calculator) {
        this.calculator = calculator
      }
    
      divide() {
        const divisor = this.calculator.peekValue()
        if (divisor === 0) {
          throw Error('Division by 0')
        }
        return this.calculator.divide()
      }
    
      putValue(value) {
        return this.calculator.putValue(value)
      }
    
      getValue() {
        return this.calculator.getValue()
      }
    
      peekValue() {
        return this.calculator.peekValue()
      }
    
      clear() {
        return this.calculator.clear()
      }
    
      multiply() {
        return this.calculator.multiply()
      }
    }
    
    const calculator = new StackCalculator()
    const safeCalculator = new SafeCalculator(calculator)
    
    calculator.putValue(3)
    calculator.putValue(2)
    console.log(calculator.multiply())  // 3 * 2 = 6
    
    safeCalculator.putValue(2)
    console.log(safeCalculator.multiply())  // 6 * 2 = 12
    
    calculator.putValue(0)
    console.log(calculator.divide())  // 12 / 0 = Infinity
    
    safeCalculator.clear()
    safeCalculator.putValue(4)
    safeCalculator.putValue(0)
    console.log(safeCalculator.divide())  // 4 / 0 -> Error
    

    在这次的实现中,proxy 拦截了感兴趣的方法(divide()),为其实现了新的行为(除以 0),而其他的操作(如 putValue()getValue()peekValue()clear()multiply())则是简单地分派给 subject 去做。
    计算器的状态(栈中存放的值)仍由 calculator 实例在维护,SafeCalculator 只是调用 calculator 的方法来读取或者修改这些状态。

    上面的实现方式,需要我们显式地将很多方法指派给 subject。即需要写出很多如下形式的代码片段:

    getValue() {
      return this.calculator.getValue()
    }
    

    这在很大程度上增加了代码的冗余度。

    Object augmentation

    对象增强(Object augmentation)又叫做猴子补丁(monkey patching),能够只代理某个对象的部分方法,并且可能是所有方案中最简单、最常见的一种。
    它可以将 subject 的某个方法直接替换为 proxy 版本的实现,即直接修改 subject 对象本身。

    参考如下代码:

    class StackCalculator {
      // see above
    }
    
    
    function patchToSafeCalculator(calculator) {
      const divideOrig = calculator.divide
      calculator.divide = () => {
        // additional validation logic
        const divisor = calculator.peekValue()
        if (divisor === 0) {
          throw Error('Division by 0')
        }
        // if valid, delegates to the subject
        return divideOrig.apply(calculator)
      }
    
      return calculator
    }
    
    const calculator = new StackCalculator()
    const safeCalculator = new patchToSafeCalculator(calculator)
    
    safeCalculator.putValue(4)
    safeCalculator.putValue(0)
    // console.log(calculator.divide())  // Error, not Infinity
    console.log(safeCalculator.divide())  // 4 / 0 -> Error
    

    当只需要代理某一个或几个方法的时候,上述方案会非常方便。用户不需要再手动重新实现一遍 putValue() 等方法。
    不幸的是,简单化也带来了一定的代价,像上面那样直接修改 subject 对象是一种危险的行为。当该 subject 对象被其他部分的代码共享时,修改行为必须尽一切可能避免,从而不至于引发意想不到的 side effect。
    尝试将代码中的 // console.log(calculator.divide()) 取消注释,会发现 calculator 并没有像之前那样输出 Infinity,而是跟 safeCalculator 一样报出错误。即原来的 calculator 对象已经被猴子补丁所改变。

    内置的 Proxy 对象

    ES2015 引入了一种原生的创建 proxy 对象的方式。其语法如下:
    const proxy = new Proxy(target, handler)

    其中 target 代表被 proxy 代理的对象(即 subject),handler 对象则用来定义 proxy 的具体行为。它包含一系列可选的预定义方法(如 getsetapply 等),叫做 trap methods,在 subject 上执行对应的操作时会自动触发这些方法。

    示例代码:

    class StackCalculator {
      // see above
    }
    
    
    const safeCalculatorHandler = {
      get: (target, property) => {
        if (property === 'divide') {
          // proxied method
          return function () {
            // additional validation logic
            const divisor = target.peekValue()
            if (divisor === 0) {
              throw Error('Division by 0')
            }
            // if valid, delegates to the subject
            return target.divide()
          }
        }
    
        // delegated methods and properties
        return target[property]
      }
    }
    
    const calculator = new StackCalculator()
    const safeCalculator = new Proxy(
      calculator,
      safeCalculatorHandler
    )
    
    
    calculator.putValue(4)
    calculator.putValue(0)
    console.log(calculator.divide())  // Infinity
    
    safeCalculator.clear()
    safeCalculator.putValue(4)
    safeCalculator.putValue(0)
    console.log(safeCalculator.divide())  // 4 / 0 -> Error
    

    在上面的代码中,通过 get trap method 捕获对于原本的 calculator 对象的属性和方法的访问,当访问的方法是 divide() 时,proxy 就会返回一个添加了额外验证逻辑的新函数。
    之后又简单地使用 target[property] 返回了所有未修改过的属性和方法。

    总的来说,Proxy 对象为我们提供了一个非常简单的方法,只代理 subject 的一部分功能,且不需要显式地将未代理的方法移交给 subject。同时也不会对原本的 subject 做出任何改动。

    几种 proxy 实现机制的比较
    • Composition:最直观和安全,subject 不会被修改。但需要手动将未代理的方法指派给 subject。冗余代码
    • Object augmentation:会直接修改原本的 subject 对象,不够安全。不需要手动处理未代理的方法
    • Proxy 对象:提供了更高级的访问控制。支持更多类型的属性访问,比如可以拦截 subject 对自身属性的删除等操作。不会修改 subject 本身,只需要使用一句代码处理未代理的方法

    实例:logging Writable stream

    mkdir logwritting && cd logwritting
    

    package.json:

    {
      "type": "module"
    }
    

    logging-writable.js:

    export function createLoggingWritable(writable) {
      return new Proxy(writable, {
        get(target, propKey) {
          if (propKey === 'write') {
            return function (...args) {
              const [chunk] = args
              console.log('Writing', chunk)
              return writable.write(...args)
            }
          }
          return target[propKey]
        }
      })
    }
    

    index.js:

    import {createWriteStream} from 'fs'
    import {createLoggingWritable} from './logging-writable.js'
    
    const writable = createWriteStream('test.txt')
    const writableProxy = createLoggingWritable(writable)
    
    writableProxy.write('First chunk')
    writableProxy.write('Second chunk')
    writable.write('This is not logged')
    writableProxy.end()
    // => Writing First chunk
    // => Writing Second chunk
    

    参考资料

    Node.js Design Patterns: Design and implement production-grade Node.js applications using proven patterns and techniques, 3rd Edition

    相关文章

      网友评论

          本文标题:Node.js 设计模式笔记 —— Proxy 模式

          本文链接:https://www.haomeiwen.com/subject/dejdurtx.html