提供一种管理实体关系的方式

  • Proxy(代理):让我们能够在用户访问另一个对象时做出管控
  • Decorator(修饰器):能够动态地增强现有对象的行为
  • Adapter(适配器):让我们能够访问到与某对象接口不同的另一个对象所具备的功能

    Proxy(代理)模式

    当用户(Client)通过 proxy 来操作 subject 的时候,proxy 会把用户想要对 subject 所执行的全部操作(或其中某些操作)拦截下来,以便增强或补充该操作的功能。
    proxy 对象与 subject 对象的接口是相同的,两者是可以互换的。
    image.png
    可以在以下情况使用:

  • 验证数据

  • 确保安全:是否有权限执行操作
  • 实现缓存机制
  • 惰性初始化:如果 subject 对象创建起来开销比较大,那就让 proxy 尽量推迟 subject 的创建时机,只有在必须用到 subject 的时候,才去真正创建它
  • 实现日志记录功能
  • 表示远程对象

    怎样实现 Proxy 模式

    ```typescript 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 } }

  1. <a name="rwM7K"></a>
  2. ### 通过组合对象来实现 Proxy 模式
  3. > 组合(composition)是把某对象与另一个对象相结合,让后者能够扩充或使用前者的功能。
  4. Proxy 模式中,是创建一个新的对象(proxy 对象),让它的接口跟原对象(subject 对象)相同,并且让 proxy 对象通过实例变量或闭包变量来保存 subject,可在创建 proxy 对象时注入,或由 proxy 自己创建。
  5. ```typescript
  6. class SafeCalculator {
  7. constructor (calculator) {
  8. this.calculator = calculator
  9. }
  10. // proxied method
  11. divide () {
  12. // additional validation logic
  13. const divisor = this.calculator.peekValue()
  14. if (divisor === 0) {
  15. throw Error('Division by 0')
  16. }
  17. // if valid delegates to the subject
  18. return this.calculator.divide()
  19. }
  20. // delegated methods
  21. putValue (value) {
  22. return this.calculator.putValue(value)
  23. }
  24. getValue () {
  25. return this.calculator.getValue()
  26. }
  27. peekValue () {
  28. return this.calculator.peekValue()
  29. }
  30. clear () {
  31. return this.calculator.clear()
  32. }
  33. multiply () {
  34. return this.calculator.multiply()
  35. }
  36. }
  37. const calculator = new StackCalculator()
  38. const safeCalculator = new SafeCalculator(calculator)
  39. calculator.putValue(3)
  40. calculator.putValue(2)
  41. console.log(calculator.multiply()) // 3*2 = 6
  42. safeCalculator.putValue(2)
  43. console.log(safeCalculator.multiply()) // 6*2 = 12
  44. calculator.putValue(0)
  45. console.log(calculator.divide()) // 12/0 = Infinity
  46. safeCalculator.clear()
  47. safeCalculator.putValue(4)
  48. safeCalculator.putValue(0)
  49. console.log(safeCalculator.divide()) // 4/0 -> Error('Division by 0')

通过工厂函数与对象字面量来创建 proxy 对象

  1. function createSafeCalculator (calculator) {
  2. return {
  3. // proxied method
  4. divide () {
  5. // additional validation logic
  6. const divisor = calculator.peekValue()
  7. if (divisor === 0) {
  8. throw Error('Division by 0')
  9. }
  10. // if valid delegates to the subject
  11. return calculator.divide()
  12. },
  13. // delegated methods
  14. putValue (value) {
  15. return calculator.putValue(value)
  16. },
  17. getValue () {
  18. return calculator.getValue()
  19. },
  20. peekValue () {
  21. return calculator.peekValue()
  22. },
  23. clear () {
  24. return calculator.clear()
  25. },
  26. multiply () {
  27. return calculator.multiply()
  28. }
  29. }
  30. }
  31. const calculator = new StackCalculator()
  32. const safeCalculator = createSafeCalculator(calculator)

通过增强原对象来实现 Proxy 模式

在原对象只有少数几个方法需要代理时,通过直接修改 subject,替换相应的方法为我们实现的代理方法

  1. function patchToSafeCalculator (calculator) {
  2. const divideOrig = calculator.divide
  3. calculator.divide = () => {
  4. // additional validation logic
  5. const divisor = calculator.peekValue()
  6. if (divisor === 0) {
  7. throw Error('Division by 0')
  8. }
  9. // if valid delegates to the subject
  10. return divideOrig.apply(calculator)
  11. }
  12. return calculator
  13. }

属于危险操作,可能会导致其他组件受到影响,且难以捕获原因。

用内置的 Proxy 对象实现 Proxy 模式

const proxy = new Proxy(target, handler);

  1. const safeCalculatorHandler = {
  2. get: (target, property) => {
  3. if (property === 'divide') {
  4. // proxied method
  5. return function () {
  6. // additional validation logic
  7. const divisor = target.peekValue()
  8. if (divisor === 0) {
  9. throw Error('Division by 0')
  10. }
  11. // if valid delegates to the subject
  12. return target.divide()
  13. }
  14. }
  15. // delegated methods and properties
  16. return target[property]
  17. }
  18. }
  19. const calculator = new StackCalculator()
  20. const safeCalculator = new Proxy(calculator, safeCalculatorHandler)

用内置的 Proxy 做出来的这种对象,会继承 subject 的 prototype(原型),因此,判断 safeCalculator instanceof StackCalculator 所得到的结果是 true

创建带有日志功能的 Writable 流

  1. export function createLoggingWritable (writable) {
  2. return new Proxy(writable, { // ①
  3. get (target, propKey, receiver) { // ②
  4. // 判断受访问属性
  5. if (propKey === 'write') { // ③
  6. return function (...args) { // ④
  7. const [chunk] = args
  8. console.log('Writing', chunk)
  9. return writable.write(...args)
  10. }
  11. }
  12. return target[propKey] // ⑤
  13. }
  14. })
  15. }
  1. import { createWriteStream } from 'fs'
  2. import { createLoggingWritable } from './logging-writable.js'
  3. const writable = createWriteStream('test.txt')
  4. const writableProxy = createLoggingWritable(writable)
  5. writableProxy.write('First chunk')
  6. writableProxy.write('Second chunk')
  7. writable.write('This is not logged')
  8. writableProxy.end()

用 Proxy 实现 Change Observer 模式

Change Observer 模式让某对象(subject)把状态的变化情况通知一个或多个 observe(观察者),令它们能够在 subject 的状态发生改变时尽快做出反应。
和 Observer 模式的区别:

  • Change Observer 的重点在于让我们能够监测对象属性上面的变化
  • Observer 模式采用 EventEmitter 播报信息,让我们知道系统里面发生了什么事件,这个事件不一定是属性上的变化

    1. export function createObservable (target, observer) {
    2. const observable = new Proxy(target, {
    3. set (obj, prop, value) {
    4. if (value !== obj[prop]) {
    5. const prev = obj[prop]
    6. obj[prop] = value
    7. // 对象发生变化时触发的函数
    8. observer({ prop, prev, curr: value })
    9. }
    10. return true
    11. }
    12. })
    13. return observable
    14. }

    ```typescript import { createObservable } from ‘./create-observable.js’

function calculateTotal (invoice) { // ① return invoice.subtotal - invoice.discount + invoice.tax }

const invoice = { subtotal: 100, discount: 10, tax: 20 } let total = calculateTotal(invoice) console.log(Starting total: ${total})

const obsInvoice = createObservable( // ② invoice, ({ prop, prev, curr }) => { total = calculateTotal(invoice) console.log(TOTAL: ${total} (${prop} changed: ${prev} -> ${curr})) } )

// ③ obsInvoice.subtotal = 200 // TOTAL: 210 obsInvoice.discount = 20 // TOTAL: 200 obsInvoice.discount = 20 // no change: doesn’t notify obsInvoice.tax = 30 // TOTAL: 210

console.log(Final total: ${total})

  1. <a name="GIML7"></a>
  2. # Decorator(修饰器)模式
  3. > 能够动态地扩充现有对象的行为。主要是给其中添加新的功能。
  4. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/651859/1653713276473-8cad0dd0-31ca-4e86-bcca-c717677854e0.png#clientId=u9d74d998-fd1b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=420&id=u930ddf03&margin=%5Bobject%20Object%5D&name=image.png&originHeight=462&originWidth=1395&originalType=binary&ratio=1&rotation=0&showTitle=false&size=69214&status=done&style=none&taskId=ua25df74e-6be9-46ef-ad17-aad85e4bf11&title=&width=1268.181790694718)
  5. <a name="H0mt0"></a>
  6. ## 实现 Decorator 模式的几种办法
  7. <a name="JE4XN"></a>
  8. ### 用组合实现 Decorator 模式
  9. 把受装饰的组件包裹在一个新的对象里面,并让这个新的对象所在的类,支持原组件所具备的那套功能。
  10. ```typescript
  11. class EnhancedCalculator {
  12. constructor (calculator) {
  13. this.calculator = calculator
  14. }
  15. // new method
  16. add () {
  17. const addend2 = this.getValue()
  18. const addend1 = this.getValue()
  19. const result = addend1 + addend2
  20. this.putValue(result)
  21. return result
  22. }
  23. // modified method
  24. divide () {
  25. // additional validation logic
  26. const divisor = this.calculator.peekValue()
  27. if (divisor === 0) {
  28. throw Error('Division by 0')
  29. }
  30. // if valid delegates to the subject
  31. return this.calculator.divide()
  32. }
  33. // ...
  34. }

通过增强原对象来实现 Decorator 模式

把新方法直接添加到受修饰的对象里面

  1. function patchCalculator (calculator) {
  2. // new method
  3. calculator.add = function () {
  4. const addend2 = calculator.getValue()
  5. const addend1 = calculator.getValue()
  6. const result = addend1 + addend2
  7. calculator.putValue(result)
  8. return result
  9. }
  10. // modified method
  11. const divideOrig = calculator.divide
  12. calculator.divide = () => {
  13. // additional validation logic
  14. const divisor = calculator.peekValue()
  15. if (divisor === 0) {
  16. throw Error('Division by 0')
  17. }
  18. // if valid delegates to the subject
  19. return divideOrig.apply(calculator)
  20. }
  21. return calculator
  22. }
  23. const calculator = new StackCalculator()
  24. const enhancedCalculator = patchCalculator(calculator)

通过 Proxy 对象实现 Decorator 模式

  1. const enhancedCalculatorHandler = {
  2. get (target, property) {
  3. if (property === 'add') {
  4. // new method
  5. return function add () {
  6. const addend2 = target.getValue()
  7. const addend1 = target.getValue()
  8. const result = addend1 + addend2
  9. target.putValue(result)
  10. return result
  11. }
  12. } else if (property === 'divide') {
  13. // modified method
  14. return function () {
  15. // additional validation logic
  16. const divisor = target.peekValue()
  17. if (divisor === 0) {
  18. throw Error('Division by 0')
  19. }
  20. // if valid delegates to the subject
  21. return target.divide()
  22. }
  23. }
  24. // delegated methods and properties
  25. return target[property]
  26. }
  27. }
  28. const calculator = new StackCalculator()
  29. const enhancedCalculator = new Proxy(calculator, enhancedCalculatorHandler)

使用 Decorator 模式来修饰 LevelUP 数据库

  1. export function levelSubscribe (db) {
  2. db.subscribe = (pattern, listener) => { // ①
  3. db.on('put', (key, val) => { // ②
  4. const match = Object.keys(pattern).every(
  5. k => (pattern[k] === val[k]) // ③
  6. )
  7. if (match) {
  8. listener(key, val) // ④
  9. }
  10. })
  11. }
  12. return db
  13. }
  1. import { dirname, join } from 'path'
  2. import { fileURLToPath } from 'url'
  3. import level from 'level'
  4. import { levelSubscribe } from './level-subscribe.js'
  5. const __dirname = dirname(fileURLToPath(import.meta.url))
  6. const dbPath = join(__dirname, 'db')
  7. const db = level(dbPath, { valueEncoding: 'json' }) // ①
  8. levelSubscribe(db) // ②
  9. db.subscribe( // ③
  10. { doctype: 'tweet', language: 'en' },
  11. (k, val) => console.log(val)
  12. )
  13. db.put('1', { // ④
  14. doctype: 'tweet',
  15. text: 'Hi',
  16. language: 'en'
  17. })
  18. db.put('2', {
  19. doctype: 'company',
  20. name: 'ACME Co.'
  21. })

Adapter(适配器)模式

Adapter 模式让我们能够通过一套与原对象不同的接口来访问原对象的功能

image.png

通过 fs 式的 API 来使用 LevelUP

构建一个针对 LevelUP API 的 adapter (适配器),让它把那套接口转换成一套与 fs 这个核心模块相兼容的接口。
保证程序每次在 adapter 上面调用 readFile() 与 writeFile() 时,能够把这些操作转换为 db.get() 与 db.put() 操作。
能够让 LevelUP 数据库充当存储后端(storage backend),并利用适配器将其转化成一种支持 fs 接口的对象,使得程序能够在这个对象上面,通过简单的文件操作来修改后端数据库中的内容。

  1. import { resolve } from 'path'
  2. export function createFSAdapter (db) {
  3. return ({
  4. // 确保该函数的接口与 fs 模块所设计的同名函数相兼容
  5. readFile (filename, options, callback) {
  6. if (typeof options === 'function') {
  7. callback = options
  8. options = {}
  9. } else if (typeof options === 'string') {
  10. options = { encoding: options }
  11. }
  12. db.get(resolve(filename), { // ①
  13. valueEncoding: options.encoding
  14. },
  15. (err, value) => {
  16. if (err) {
  17. if (err.type === 'NotFoundError') { // ②
  18. err = new Error(`ENOENT, open "${filename}"`)
  19. err.code = 'ENOENT'
  20. err.errno = 34
  21. err.path = filename
  22. }
  23. return callback && callback(err)
  24. }
  25. callback && callback(null, value) // ③
  26. })
  27. },
  28. writeFile (filename, contents, options, callback) {
  29. if (typeof options === 'function') {
  30. callback = options
  31. options = {}
  32. } else if (typeof options === 'string') {
  33. options = { encoding: options }
  34. }
  35. db.put(resolve(filename), contents, {
  36. valueEncoding: options.encoding
  37. }, callback)
  38. }
  39. })
  40. }
  1. import { dirname, join } from 'path'
  2. import { fileURLToPath } from 'url'
  3. import level from 'level'
  4. import { createFSAdapter } from './fs-adapter.js'
  5. const __dirname = dirname(fileURLToPath(import.meta.url))
  6. const db = level(join(__dirname, 'db'), {
  7. valueEncoding: 'binary'
  8. })
  9. const fs = createFSAdapter(db)
  10. fs.writeFile('file.txt', 'Hello!', () => {
  11. fs.readFile('file.txt', { encoding: 'utf8' }, (err, res) => {
  12. if (err) {
  13. return console.error(err)
  14. }
  15. console.log(res)
  16. })
  17. })
  18. // try to read a missing file
  19. fs.readFile('missing.txt', { encoding: 'utf8' }, (err, res) => {
  20. console.error(err)
  21. })

可以用类似的技术,让某些使用 fs 式 API 的代码,能够同时在 Node.js 平台与浏览器平台运行。

参考

  1. github