模板

我们要分析的下一个模式称为模板,它与策略模式有很多共同点。 模板模式定义了一个抽象类,该类实现组件的骨架(表示公共部分),其中某些步骤未定义。 然后,子类可以通过实现缺失的部分(称为模板方法)来填补组件中的空白。 此模式的目的是使定义一系列类成为可能,这些类都是一系列组件的变体。 下面的 UML 图显示了我们刚刚描述的结构:

image 2024 05 07 15 23 19 552
Figure 1. 图 9.4:Template 模式的 UML 图

图 9.4 中所示的三个具体类扩展了模板类并提供了 templateMethod() 的实现,用 C++ 术语来说,它是抽象的或纯虚拟的。 在 JavaScript 中,我们没有正式的方法来定义抽象类,所以我们所能做的就是让方法未定义,或者将其分配给一个总是抛出异常的函数,表明该方法必须被实现。 模板模式可以被认为是比我们迄今为止看到的其他模式更传统的面向对象模式,因为继承是其实现的核心部分。

Template 和 Strategy 的目的非常相似,但两者的主要区别在于它们的结构和实现。 两者都允许我们在重用公共部分的同时更改组件的可变部分。 然而,虽然策略允许我们在运行时动态地执行此操作,但使用模板时,完整的组件在定义具体类时就确定了。 在这些假设下,模板模式可能更适合我们想要创建组件的预打包变体的情况。 与往常一样,一种模式和另一种模式之间的选择取决于开发人员,他们必须考虑每个用例的各种优缺点。

现在让我们来看一个例子。

一个配置管理器模板

为了更好地了解策略和模板之间的差异,现在让我们重新实现我们在策略模式部分中定义的 Config 对象,但这次使用模板。 与之前版本的 Config 对象一样,我们希望能够使用不同的文件格式加载和保存一组配置属性。

让我们从定义模板类开始。 我们将其称为 ConfigTemplate:

import { promises as fsPromises } from 'fs'
import objectPath from 'object-path'

export class ConfigTemplate {
    async load (file) {
        console.log(`Deserializing from ${file}`)
        this.data = this._deserialize(
            await fsPromises.readFile(file, 'utf-8'))
    }

    async save (file) {
        console.log(`Serializing to ${file}`)
        await fsPromises.writeFile(file, this._serialize(this.data))
    }

    get (path) {
        return objectPath.get(this.data, path)
    }

    set (path, value) {
        return objectPath.set(this.data, path, value)
    }

    _serialize () {
        throw new Error('_serialize() must be implemented')
    }

    _deserialize () {
        throw new Error('_deserialize() must be implemented')
    }
}

ConfigTemplate 类实现配置管理逻辑的公共部分,即设置和获取属性,以及加载和保存到磁盘。 但是,它使 _serialize() 和 _deserialize() 的实现保持开放状态; 这些实际上是我们的模板方法,它将允许创建支持特定配置格式的具体 Config 类。 模板方法名称开头的下划线表示它们仅供内部使用,这是标记受保护方法的简单方法。 由于在 JavaScript 中我们无法将方法声明为抽象方法,因此我们只需将它们定义为存根,如果调用它们(换句话说,如果它们没有被具体子类覆盖),则会抛出错误。

现在让我们使用模板创建一个具体的类,例如,允许我们使用 JSON 格式加载和保存配置的类:

import { ConfigTemplate } from './configTemplate.js'

export class JsonConfig extends ConfigTemplate {
    _deserialize (data) {
        return JSON.parse(data)
    }

    _serialize (data) {
        return JSON.stringify(data, null, ' ')
    }
}

JsonConfig 类扩展了我们的模板类 ConfigTemplate,并为 _deserialize() 和 _serialize() 方法提供了具体实现。

同样,我们可以使用相同的模板类实现支持 .ini 文件格式的 IniConfig 类:

import { ConfigTemplate } from './configTemplate.js'
import ini from 'ini'

export class IniConfig extends ConfigTemplate {
    _deserialize (data) {
        return ini.parse(data)
    }

    _serialize (data) {
        return ini.stringify(data)
    }
}

现在我们可以使用具体的配置管理器类来加载和保存一些配置数据:

import { JsonConfig } from './jsonConfig.js'
import { IniConfig } from './iniConfig.js'

async function main () {
    const jsonConfig = new JsonConfig()
    await jsonConfig.load('samples/conf.json')
    jsonConfig.set('nodejs', 'design patterns')
    await jsonConfig.save('samples/conf_mod.json')

    const iniConfig = new IniConfig()
    await iniConfig.load('samples/conf.ini')
    iniConfig.set('nodejs', 'design patterns')
    await iniConfig.save('samples/conf_mod.ini')
}

main()

请注意与策略模式的区别:格式化和解析配置数据的逻辑被烘焙到类本身中,而不是在运行时选择。

模板模式允许我们通过重用从父模板类继承的逻辑和接口并仅提供一些抽象方法的实现,以最小的努力获得一个新的、完全工作的配置管理器。

in the wild

这种模式对我们来说不应该是全新的。 当我们扩展不同的流类来实现自定义流时,我们已经在第 6 章 “使用流进行编码” 中遇到过它。 在这种情况下,模板方法是 _write()、_read()、_transform() 或 _flush() 方法,具体取决于我们想要实现的流类。 要创建新的自定义流,我们需要继承特定的抽象流类,为模板方法提供实现。

接下来,我们将学习 JavaScript 语言本身内置的一个非常重要且普遍存在的模式,即迭代器模式。