装饰器

装饰器是一种结构设计模式,旨在动态增强现有对象的行为。 它与经典继承不同,因为行为不会添加到同一类的所有对象,而只会添加到显式修饰的实例。

在实现方面,它与代理模式非常相似,但它不是增强或修改对象现有接口的行为,而是用新功能对其进行增强,如图 8.2 所示:

image 2024 05 07 14 36 51 183
Figure 1. 图 8.2:装饰器模式示意图

在图 8.2 中,Decorator 对象通过添加 methodC() 操作来扩展 Component 对象。 现有方法通常被委托给装饰对象而不进行进一步处理,但在某些情况下,它们也可能被拦截并通过额外的行为进行增强。

实现装饰器的技术

尽管代理和装饰器在概念上是两种不同的模式,具有不同的意图,但它们实际上共享相同的实现策略。 我们将很快对其进行审查。 这次我们希望使用 Decorator 模式来获取 StackCalculator 类的实例并 “装饰它”,以便它还公开一个名为 add() 的新方法,我们可以使用该方法在两个数字之间执行加法。 我们还将使用装饰器拦截所有对 divide() 方法的调用,并实现我们在 SafeCalculator 示例中已经看到的相同的除零检查。

组合

使用组合,装饰组件被包装在通常继承自它的新对象周围。 在这种情况下,装饰器只需要定义新方法,同时将现有方法委托给原始组件:

class EnhancedCalculator {
    constructor (calculator) {
        this.calculator = calculator
    }

    // new method
    add () {
        const addend2 = this.getValue()
        const addend1 = this.getValue()
        const result = addend1 + addend2
        this.putValue(result)
        return result
    }

    // modified method
    divide () {
        // additional validation logic
        const divisor = this.calculator.peekValue()
        if (divisor === 0) {
            throw Error('Division by 0')
        }
        // if valid delegates to the subject
        return this.calculator.divide()
    }

    // delegated methods
    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 enhancedCalculator = new EnhancedCalculator(calculator)

enhancedCalculator.putValue(4)
enhancedCalculator.putValue(3)
console.log(enhancedCalculator.add()) // 4+3 = 7
enhancedCalculator.putValue(2)
console.log(enhancedCalculator.multiply()) // 7*2 = 14

如果您还记得代理模式的组合实现,您可能会发现这里的代码看起来非常相似。

我们创建了新的 add() 方法并增强了原始 divide() 方法的行为(有效地复制了我们在前面的 SafeCalculator 示例中看到的功能)。 最后,我们将 putValue()、getValue()、peekValue()、clear() 和 multiply() 方法委托给原始主题。

对象增强

对象装饰也可以通过简单地将新方法直接附加到装饰对象(猴子修补)来实现,如下所示:

function patchCalculator (calculator) {
    // new method
    calculator.add = function () {
        const addend2 = calculator.getValue()
        const addend1 = calculator.getValue()
        const result = addend1 + addend2
        calculator.putValue(result)
        return result
    }

    // modified method
    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 enhancedCalculator = patchCalculator(calculator)
// ...

请注意,在此示例中,calculator 和 enhancedCalculator 引用同一对象(calculator ==enhancedCalculator)。 这是因为 patchCalculator() 正在改变原始计算器对象,然后返回它。 您可以通过调用 calculator.add() 或 calculator.divide() 来确认这一点。

使用 Proxy 对象进行装饰

可以使用 Proxy 对象来实现对象装饰。 一个通用示例可能如下所示:

const enhancedCalculatorHandler = {
    get (target, property) {
        if (property === 'add') {
            // new method
            return function add () {
                const addend2 = target.getValue()
                const addend1 = target.getValue()
                const result = addend1 + addend2
                target.putValue(result)
                return result
            }
        } else if (property === 'divide') {
            // modified 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 enhancedCalculator = new Proxy(
    calculator,enhancedCalculatorHandler
)
// ...

如果我们要比较这些不同的实现,在分析代理模式期间讨论的相同注意事项也适用于装饰器。 让我们专注于通过现实生活中的示例来练习该模式!

装饰 LevelUP 数据库

在开始编写下一个示例之前,让我们先简单介绍一下我们现在要使用的模块 LevelUP。

LevelUP 和 LevelDB 简介

LevelUP (nodejsdp.link/levelup) 是 Google LevelDB 的 Node.js 包装器,LevelDB 是一个键值存储,最初是为了在 Chrome 浏览器中实现 IndexedDB 而构建的,但它的功能远不止于此。 LevelDB 因其极简主义和可扩展性而被定义为“数据库中的 Node.js”。 与 Node.js 一样,LevelDB 提供了极快的性能和最基本的功能集,允许开发人员在其之上构建任何类型的数据库。

Node.js 社区(这里是 Rod Vagg)并没有错过通过创建 LevelUP 将这个数据库的强大功能带入 Node.js 世界的机会。 它最初是作为 LevelDB 的包装器诞生的,后来演变为支持多种后端,从内存存储到其他 NoSQL 数据库(例如 Riak 和 Redis),再到 Web 存储引擎(例如 IndexedDB 和 localStorage),从而允许我们使用相同的 API 在服务器和客户端上,开辟了一些非常有趣的场景。

如今,LevelUP 周围有一个由插件和模块组成的庞大生态系统,这些插件和模块扩展了微小的核心以实现复制、二级索引、实时更新、查询引擎等功能。 完整的数据库也建立在 LevelUP 之上,包括 CouchDB 克隆,例如 PouchDB (nodejsdp.link/pouchdb),甚至还有图形数据库 LevelGraph (nodejsdp.link/levelgraph),它可以在 Node.js 和浏览器上运行 !

要了解有关 LevelUP 生态系统的更多信息,请访问 nodejsdp.link/awesome-level。

实现 LevelUP 插件

在下一个示例中,我们将向您展示如何使用装饰器模式为 LevelUP 创建一个简单的插件,特别是对象增强技术,这是装饰对象的最简单但也是最实用和有效的方法 额外的功能。

为了方便起见,我们将使用 level 包 (nodejsdp.link/level),它捆绑了 levelup 和名为 leveldown 的默认适配器,该适配器使用 LevelDB 作为后端。

我们想要构建的是 LevelUP 的一个插件,它允许我们在每次将具有特定模式的对象保存到数据库中时接收通知。 例如,如果我们订阅诸如 {a: 1} 之类的模式,我们希望在保存诸如 {a: 1, b: 3} 或 {a: 1, c: 'x'} 之类的对象时收到通知 进入数据库。

让我们开始通过创建一个名为 levelsubscribe.js 的新模块来构建我们的小插件。 然后我们将插入以下代码:

export function levelSubscribe (db) {
    db.subscribe = (pattern, listener) => { // (1)
        db.on('put', (key, val) => { // (2)
            const match = Object.keys(pattern).every(
                k => (pattern[k] === val[k]) // (3)
            )
            if (match) {
                listener(key, val) // (4)
            }
        })
    }

    return db
}

这就是我们的插件; 这非常简单。 我们简单分析一下前面的代码:

  1. 我们用一个名为 subscribe() 的新方法来修饰 db 对象。 我们只需将该方法直接附加到提供的数据库实例(对象增强)。

  2. 我们监听对数据库执行的任何 put 操作。

  3. 我们执行一个非常简单的模式匹配算法,该算法验证所提供模式中的所有属性在插入的数据中也可用。

  4. 如果有匹配,我们会通知监听者。

现在让我们编写一些代码来尝试我们的新插件:

import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import level from 'level'
import { levelSubscribe } from './level-subscribe.js'

const __dirname = dirname(fileURLToPath(import.meta.url))

const dbPath = join(__dirname, 'db')
const db = level(dbPath, { valueEncoding: 'json' }) // (1)
levelSubscribe(db) // (2)

db.subscribe( // (3)
    { doctype: 'tweet', language: 'en' },
    (k, val) => console.log(val)
)
db.put('1', { // (4)
    doctype: 'tweet',
    text: 'Hi',
    language: 'en'
})
db.put('2', {
    doctype: 'company',
    name: 'ACME Co.'
})

前面的代码是这样工作的:

  1. 首先,我们初始化 LevelUP 数据库,选择存储文件的目录以及值的默认编码。

  2. 然后,我们附加我们的插件,它装饰原始的数据库对象。

  3. 此时,我们已经准备好使用插件提供的新功能,即 subscribe() 方法,其中我们指定我们对所有具有 doctype: 'tweet' 和 language: 'en' 的对象感兴趣 。

  4. 最后,我们使用 put 将一些值保存到数据库中。 第一个调用会触发与我们的订阅相关的回调,我们应该看到存储的对象打印到控制台。 这是因为,在这种情况下,对象与订阅匹配。 第二次调用不会生成任何输出,因为存储的对象与订阅条件不匹配。

此示例展示了装饰器模式的最简单实现的实际应用,即对象增强。 它可能看起来像一个微不足道的模式,但如果使用得当,它无疑具有强大的力量。

为简单起见,我们的插件仅与 put 操作结合使用,但它可以轻松扩展,甚至可以与批处理操作一起使用 (nodejsdp.link/levelup-batch)。

In the wild

有关如何在现实世界中使用装饰器的更多示例,您可以检查更多 LevelUP 插件的代码:

  • level-inverted-index (nodejsdp.link/level-inverted-index):这是一个添加倒排索引的插件 到 LevelUP 数据库,允许我们对数据库中存储的值执行简单的文本搜索

  • levelplus (nodejsdp.link/levelplus):这是一个向 LevelUP 数据库添加原子更新的插件

除了 LevelUP 插件之外,以下项目是 还有采用装饰器模式的好例子:

  • json-socket (nodejsdp.link/json-socket):该模块使通过 TCP(或 Unix)套接字发送 JSON 数据变得更容易。 它旨在装饰 net.Socket 的现有实例,并通过其他方法和行为来丰富该实例。

  • fastify (nodejsdp.link/fastify) 是一个Web 应用程序框架,它公开一个API,用附加功能或配置来装饰Fastify 服务器实例。 通过这种方法,应用程序的不同部分可以访问附加功能。 这是装饰器模式的一个相当通用的实现。 查看专用文档页面以了解更多信息:nodejsdp.link/fastify-decorators。