创建型(creational)设计模式,用于解决对象的创建问题。

  • Factory(工厂)模式:把对象的创建逻辑封装成函数
  • Revealing Constructor 模式:在创建对象的时候,把私有的属性与方法暂时暴露出来,并在创建完毕后隐藏
  • Builder 模式:能够简化复杂对象的创建过程
  • Singleton 模式与 Dependency Injection 模式:帮助开发者在应用程序中整理各模块之间的关系

    Factory (工厂)模式

    把对象的创建流程与该流程的实现方式解耦

    工厂模式封装的是创建新实例的逻辑,让开发者能够在该模式内部,灵活地调控这个实例的创建手法。

可以在工厂里面选用 new 运算符创建某个类的实例,可以利用闭包动态构建一个有状态的对象字面量(stateful object literal),可以根据某项条件返回一种类型的对象。
工厂的使用者,不需要知道实例如何创建。
直接通过 new 创建对象,相当于把对象的创建方式定死了

  1. export class Image {
  2. constructor (path) {
  3. this.path = path
  4. }
  5. }
  1. import { Image } from './image.js'
  2. export class ImageGif extends Image {
  3. constructor (path) {
  4. if (!path.match(/\.gif/)) {
  5. throw new Error(`${path} is not a GIF image`)
  6. }
  7. super(path)
  8. }
  9. }
  1. import { Image } from './image.js'
  2. export class ImageJpeg extends Image {
  3. constructor (path) {
  4. if (!path.match(/\.jpe?g$/)) {
  5. throw new Error(`${path} is not a JPEG image`)
  6. }
  7. super(path)
  8. }
  9. }
  1. import { ImageGif } from './imageGif.js'
  2. import { ImageJpeg } from './imageJpeg.js'
  3. import { ImagePng } from './imagePng.js'
  4. function createImage (name) {
  5. if (name.match(/\.jpe?g$/)) {
  6. return new ImageJpeg(name)
  7. } else if (name.match(/\.gif$/)) {
  8. return new ImageGif(name)
  9. } else if (name.match(/\.png$/)) {
  10. return new ImagePng(name)
  11. } else {
  12. throw new Error('Unsupported format')
  13. }
  14. }
  15. const image1 = createImage('photo.jpg')
  16. const image2 = createImage('photo.gif')
  17. const image3 = createImage('photo.png')
  18. console.log(image1, image2, image3)

强化封装效果

利用闭包,把工厂当作封装(encapsulation)机制使用

封装指的是控制组件的内部细节,让外部代码无法直接操纵这些细节。外界要想跟这个组件交互,只能通过公开的接口来做,这样可以让外部代码不会因为该组件的实现细节发生变化,而受到影响。

  1. function createPerson (name) {
  2. const privateProperties = {}
  3. // 只能通过 person 对象提供的接口来操纵 privateProperties 对象
  4. const person = {
  5. setName (name) {
  6. if (!name) {
  7. throw new Error('A person must have a name')
  8. }
  9. privateProperties.name = name
  10. },
  11. getName () {
  12. return privateProperties.name
  13. }
  14. }
  15. person.setName(name)
  16. return person
  17. }
  18. const person = createPerson('James Joyce')
  19. console.log(person.getName(), person)

实现封装的一些技巧:

  • 用 WeakMap 实现
  • 用 Symbol 实现
  • 在构造器里面定义私有变量
  • 用特定的命名格式(“_”下划线),只是一种约定,无实际限制功能
  • 在类里面设计私有字段(给字段名称前加上 #),实验阶段

    构建一款简单的 code profiler(代码测评工具)

    ```typescript class Profiler { constructor (label) { this.label = label this.lastTime = null }

    start () { this.lastTime = process.hrtime() }

    end () { const diff = process.hrtime(this.lastTime) console.log(Timer "${this.label}" took ${diff[0]} seconds +

    1. `and ${diff[1]} nanoseconds.`)

    } }

const noopProfiler = { start () {}, end () {} }

// 根据环境抛出不同实例的工厂函数 export function createProfiler (label) { if (process.env.NODE_ENV === ‘production’) { return noopProfiler }

return new Profiler(label) }

  1. ```typescript
  2. import { createProfiler } from './profiler.js'
  3. function getAllFactors (intNumber) {
  4. const profiler = createProfiler(
  5. `Finding all factors of ${intNumber}`)
  6. profiler.start()
  7. const factors = []
  8. for (let factor = 2; factor <= intNumber; factor++) {
  9. while ((intNumber % factor) === 0) {
  10. factors.push(factor)
  11. intNumber = intNumber / factor
  12. }
  13. }
  14. profiler.end()
  15. return factors
  16. }
  17. const myNumber = process.argv[2]
  18. const myFactors = getAllFactors(myNumber)
  19. console.log(`Factors of ${myNumber} are: `, myFactors)

Builder (生成器/建造者)模式

  • 主要目标:让开发者不用直接去调用复杂的构造器,而是在辅助方法的引导下,一步步构造对象,提高阅读性和便于管理
  • 如果构造器的某几个参数具有联动关系,可以设计辅助方法来统一指定这些参数
  • 如果某个参数的取值可以通过另一个参数推导出来,应该提供相关的 setter 方法(设置器方法),让开发者只需要指定后面那个参数就行,前面的那个参数的取值,放在 setter 方法内部去计算
  • 如果有必要,在把相关的值传给要构建的那个类的构造器之前,多执行些处理(例如类型转换、正规化、或其他一些验证)

    1. export class Url {
    2. constructor (protocol, username, password, hostname,
    3. port, pathname, search, hash) {
    4. this.protocol = protocol
    5. this.username = username
    6. this.password = password
    7. this.hostname = hostname
    8. this.port = port
    9. this.pathname = pathname
    10. this.search = search
    11. this.hash = hash
    12. this.validate()
    13. }
    14. validate () {
    15. if (!this.protocol || !this.hostname) {
    16. throw new Error('Must specify at least a ' +
    17. 'protocol and a hostname')
    18. }
    19. }
    20. toString () {
    21. let url = ''
    22. url += `${this.protocol}://`
    23. if (this.username && this.password) {
    24. url += `${this.username}:${this.password}@`
    25. }
    26. url += this.hostname
    27. if (this.port) {
    28. url += this.port
    29. }
    30. if (this.pathname) {
    31. url += this.pathname
    32. }
    33. if (this.search) {
    34. url += `?${this.search}`
    35. }
    36. if (this.hash) {
    37. url += `#${this.hash}`
    38. }
    39. return url
    40. }
    41. }

    ```typescript import { Url } from ‘./url.js’

export class UrlBuilder { setProtocol (protocol) { this.protocol = protocol return this }

setAuthentication (username, password) { this.username = username this.password = password return this }

setHostname (hostname) { this.hostname = hostname return this }

setPort (port) { this.port = port return this }

setPathname (pathname) { this.pathname = pathname return this }

setSearch (search) { this.search = search return this }

setHash (hash) { this.hash = hash return this }

build () { return new Url(this.protocol, this.username, this.password, this.hostname, this.port, this.pathname, this.search, this.hash) } }

  1. ```typescript
  2. import { UrlBuilder } from './urlBuilder.js'
  3. const url = new UrlBuilder()
  4. .setProtocol('https')
  5. .setAuthentication('user', 'pass')
  6. .setHostname('example.com')
  7. .build()
  8. console.log(url.toString())

Revealing Constructor 模式

它想要解决的难题,就是类的设计者怎么向开发者 “reveal”(展示或纰漏)某些私密的功能,让他只能在创建该类的对象时使用这些功能,一旦创建完毕,就不能再针对这个对象调用这些功能了。

存在下面三种情况:

  • 设计者想让这个对象只能在创建的时候受到修改
  • 设计者允许用户定制这个对象的行为,但只想让他在创建该对象时给予定制,而不想让他在创建完之后重新定义此行为
  • 设计者想让这个对象只能在创建时初始化一次,而不能在创建好了之后又给予重置

    构建不可变的缓冲区

    不可变(immutable)对象是指这个对象创建出来后,它的数据或状态就不能再修改了。
    把这种不可变的对象传给其他程序库或函数之前,不用创建防御式的拷贝(defensive copy) ```typescript const MODIFIER_NAMES = [‘swap’, ‘write’, ‘fill’]

export class ImmutableBuffer { constructor (size, executor) { const buffer = Buffer.alloc(size) // 设置缓冲区大小 const modifiers = {} // 存储能够修改 buffer 的那些方法 for (const prop in buffer) { if (typeof buffer[prop] !== ‘function’) { continue }

  1. // 判断该属性能够修改 buffer
  2. if (MODIFIER_NAMES.some(m => prop.startsWith(m))) {
  3. modifiers[prop] = buffer[prop].bind(buffer)
  4. } else {
  5. // 不能修改 buffer 的属性直接挂载到当前对象上
  6. this[prop] = buffer[prop].bind(buffer)
  7. }
  8. }
  9. // 让用户通过 modifiers 所存在的属性来修改 ImmutableBuffer 对象内部的 buffer
  10. executor(modifiers)

} }

  1. 这个 ImmutableBuffer 是在使用它的人与它内部的那个普通 buffer 对象之间,充当代理。在 buffer 实例所能提供的方法里面,只读的那些方法,可以直接通过 ImmutableBuffer 接口调用,其他的可能修改内容的方法,则只能在 executor 函数里面使用
  2. ```typescript
  3. import { ImmutableBuffer } from './immutableBuffer.js'
  4. const hello = 'Hello!'
  5. const immutable = new ImmutableBuffer(hello.length,
  6. ({ write }) => {
  7. write(hello)
  8. })
  9. console.log(String.fromCharCode(immutable.readInt8(0)))
  10. // the following line will throw
  11. // "TypeError: immutable.write is not a function"
  12. // immutable.write('Hello?')

revealing constructor 模式的大概模型如下:

  1. const object = new SomeClass(function executor(revealedMembers) {
  2. // 用户在这里编写代码,通过 revealedMembers 所展示的私密功能操纵本对象
  3. })

Singleton(单例)模式

需要某个类只存在一个实例的情况:

  • 想要共用状态信息,不想让同一个实体由好几个对象来表示,那样会让那些对象的状态各不相同
  • 想要优化资源的使用逻辑,不想让用户创建出好多个这样的这个资源
  • 想确保程序对某资源所做的访问操作总是同步的,不想让多项操作在同一时刻争抢这份资源 ```typescript /**

    • The Singleton class defines the getInstance method that lets clients access
    • the unique singleton instance. */ class Singleton { private static instance: Singleton;

      /**

      • The Singleton’s constructor should always be private to prevent direct
      • construction calls with the new operator. */ private constructor() { }

      /**

      • The static method that controls the access to the singleton instance. *
      • This implementation let you subclass the Singleton class while keeping
      • just one instance of each subclass around. */ public static getInstance(): Singleton { if (!Singleton.instance) {

        1. Singleton.instance = new Singleton();

        }

        return Singleton.instance; }

      /**

      • Finally, any singleton should define some business logic, which can be
      • executed on its instance. */ public someBusinessLogic() { // … } }

/**

  • The client code. */ function clientCode() { const s1 = Singleton.getInstance(); const s2 = Singleton.getInstance();

    if (s1 === s2) {

    1. console.log('Singleton works, both variables contain the same instance.');

    } else {

    1. console.log('Singleton failed, variables contain different instances.');

    } }

clientCode();

  1. Node.js 中的单例
  2. ```typescript
  3. import { Database } from './Database.js'
  4. export const dbInstance = new Database('my-app-db', {
  5. url: 'localhost:5432',
  6. username: 'user',
  7. password: 'password'
  8. })

无论哪个包引入 dbInstance 模块,使用的都是同一个实例。因为 Node.js 加载完某个模块后,会把它缓存起来,以后即使再次引入该模块,系统也只需要从缓存中取出这个模块,而不会重新将代码执行一遍,因此不用担心程序中会出现多个 Database 实例

管理模块之间的依赖关系

用 Singleton 模块管理模块之间的依赖关系

  1. import { dirname, join } from 'path'
  2. import { fileURLToPath } from 'url'
  3. import sqlite3 from 'sqlite3'
  4. const __dirname = dirname(fileURLToPath(import.meta.url))
  5. export const db = new sqlite3.Database(
  6. join(__dirname, 'data.sqlite'))
  1. import { promisify } from 'util'
  2. import { db } from './db.js'
  3. const dbRun = promisify(db.run.bind(db))
  4. const dbAll = promisify(db.all.bind(db))
  5. export class Blog {
  6. initialize () {
  7. const initQuery = `CREATE TABLE IF NOT EXISTS posts (
  8. id TEXT PRIMARY KEY,
  9. title TEXT NOT NULL,
  10. content TEXT,
  11. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  12. );`
  13. return dbRun(initQuery)
  14. }
  15. createPost (id, title, content, createdAt) {
  16. return dbRun('INSERT INTO posts VALUES (?, ?, ?, ?)',
  17. id, title, content, createdAt)
  18. }
  19. getAllPosts () {
  20. return dbAll('SELECT * FROM posts ORDER BY created_at DESC')
  21. }
  22. }
  1. import { Blog } from './blog.js'
  2. async function main () {
  3. const blog = new Blog()
  4. await blog.initialize()
  5. const posts = await blog.getAllPosts()
  6. if (posts.length === 0) {
  7. console.log('No post available. Run `node import-posts.js`' +
  8. ' to load some sample posts')
  9. }
  10. for (const post of posts) {
  11. console.log(post.title)
  12. console.log('-'.repeat(post.title.length))
  13. console.log(`Published on ${new Date(post.created_at)
  14. .toISOString()}`)
  15. console.log(post.content)
  16. }
  17. }
  18. main().catch(console.error)

用 DI (依赖注入)管理模块之间的依赖关系

依赖注入(dependency injection,DI)模式,它让某个外部实体,把某组件所要依赖的东西输入给这个组件,这样的外部实体,通常称为 injector(注入者/注入方)。

injector 负责初始化各种组件,并把它们正确地联系起来。使用 injector 的主要优势,在于能够降低耦合程度,如果某个模块所要依赖的另外一个模块,我们不会把某个模块所要依赖的东西,直接写在该模块自己的代码里,而是由外界把这个东西传给该模块。

  1. import { promisify } from 'util'
  2. export class Blog {
  3. constructor (db) {
  4. this.db = db
  5. this.dbRun = promisify(db.run.bind(db))
  6. this.dbAll = promisify(db.all.bind(db))
  7. }
  8. initialize () {
  9. const initQuery = `CREATE TABLE IF NOT EXISTS posts (
  10. id TEXT PRIMARY KEY,
  11. title TEXT NOT NULL,
  12. content TEXT,
  13. created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  14. );`
  15. return this.dbRun(initQuery)
  16. }
  17. createPost (id, title, content, createdAt) {
  18. return this.dbRun('INSERT INTO posts VALUES (?, ?, ?, ?)',
  19. id, title, content, createdAt)
  20. }
  21. getAllPosts () {
  22. return this.dbAll(
  23. 'SELECT * FROM posts ORDER BY created_at DESC')
  24. }
  25. }
  1. import sqlite3 from 'sqlite3'
  2. export function createDb (dbFile) {
  3. return new sqlite3.Database(dbFile)
  4. }
  1. import { dirname, join } from 'path'
  2. import { fileURLToPath } from 'url'
  3. import { Blog } from './blog.js'
  4. import { createDb } from './db.js'
  5. const __dirname = dirname(fileURLToPath(import.meta.url))
  6. async function main () {
  7. // 数据库改由参数来进行构建
  8. const db = createDb(join(__dirname, 'data.sqlite'))
  9. // 注入我们自定义的数据库实例,将 blog 和 数据库彻底解耦
  10. const blog = new Blog(db)
  11. await blog.initialize()
  12. const posts = await blog.getAllPosts()
  13. if (posts.length === 0) {
  14. console.log('No post available. Run `node import-posts.js`' +
  15. ' to load some sample posts')
  16. }
  17. for (const post of posts) {
  18. console.log(post.title)
  19. console.log('-'.repeat(post.title.length))
  20. console.log(`Published on ${new Date(post.created_at)
  21. .toISOString()}`)
  22. console.log(post.content)
  23. }
  24. }
  25. main().catch(console.error)

参考资料

  1. GitHub