Node.js 设计模式笔记 —— 工厂模式

工厂(Factory)模式 是 Node.js 中最常见的设计模式之一。
其具有以下优势:

  • 将对象的创建过程与对象的实现细节进行解耦。由工厂创建一系列对象,某个对象继承的特征在运行时确定
  • 工厂模式允许我们对外暴露更少的接口。一个类可以被扩展或者操控,而工厂本身仅仅是一个负责创建对象的函数,没有给用户其他选项,从而使接口更健壮和容易理解
  • 借助闭包可以帮助强化对象的封装

解耦对象的创建和实现

工厂模式封装了新对象的创建过程,给这个过程提供了更多的灵活性和控制。在工厂内部我们可以选择各种不同的方式来创建某个对象的实例,工厂的消费者对于这些细节一无所知。
相反地,使用 new 关键字则会将代码绑定到一种特定的创建方式上。

比如下面的一个用于创建 Image 对象的工厂函数:

1
2
3
4
function createImage (name) {
return new Image(name)
}
const image = createImage('photo.jpeg')

上述 createImage 工厂函数看上去完全没有必要,直接使用如下一行代码就可以搞定:

1
const image = new Image('photo.jpeg')

按照前面所说,new 关键字会将代码绑定给一种特定类型的对象,在这里就是 Image 类型。
而工厂模式则更加灵活。假设需要重构 Image 类,将其分割成几个更小的类型,对应不同的图片格式。
工厂函数 createImage 作为唯一的创建新图片对象的方式,即便需要创建的图片对象添加了更多的类型,也可以很简单地只对 createImage 的内部逻辑进行重写,其对外开放的接口不会发生改变,不会破坏任何现有的代码:

1
2
3
4
5
6
7
8
9
10
11
function createImage(name) {
if (name.match(/\.jpe?g$/)) {
return new ImageJpeg(name)
} else if (name.match(/\.gif$/)) {
return new ImageGif(name)
} else if (name.match(/\.png$/)) {
return new ImagePng(name)
} else {
throw new Error('Unsupported format')
}
}

强化封装

借助闭包,工厂模式可以成为一种强化封装性的机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function createPerson (name) {
const privateProperties = {}

const person = {
setName (name) {
if (!name) {
throw new Error('A person must have a name')
}
privateProperties.name = name
},
getName () {
return privateProperties.name
}
}

person.setName(name)
return person
}

person = createPerson('John')
console.log(person.getName())
// => John
person.setName('Michael')
console.log(person.getName())
// => Michael

createPerson 工厂函数创建了一个 person 对象。由于闭包的存在,即便 createPerson 函数运行完毕退出了,其属性 privateProperties 仍可以被 person 对象通过其 setNamegetName 方法访问。
但与此同时,该 privateProperties 属性无法被任何外部对象(包括 person)直接访问。

完整实例:Profiler

创建并进入一个新的 simple_profiler 文件夹,编辑如下内容的 package.json 文件:

1
2
3
{
"type": "module"
}

创建如下内容的 profiler.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 ` + `and ${diff[1]} nanoseconds.`)
}
}

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

export function createProfiler (label) {
if (process.env.NODE_ENV === 'production') {
return noopProfiler
}

return new Profiler(label)
}

创建如下内容的 index.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { createProfiler } from './profiler.js'

function getAllFactors (intNumber) {
const profiler = createProfiler(
`Finding all factors of ${intNumber}`
)

profiler.start()
const factors = []
for (let factor = 2; factor <= intNumber; factor++) {
while ((intNumber % factor) === 0) {
factors.push(factor)
intNumber = intNumber / factor
}
}
profiler.end()

return factors
}

const myNumber = process.argv[2]
const myFactors = getAllFactors(myNumber)
console.log(`Factors of ${myNumber} are: `, myFactors)

运行效果:

1
2
3
4
5
$ NODE_ENV=production node index.js 2201307499
Factors of 2201307499 are: [ 38737, 56827 ]
$ node index.js 2201307499
Timer "Finding all factors of 2201307499" took 0 seconds and 9738800 nanoseconds.
Factors of 2201307499 are: [ 38737, 56827 ]

简单来说,就是通过 createProfiler 工厂函数来创建不同的 Profiler 对象。若环境变量 NODE_ENV 的值为 production,则返回一个新的的 noopProfiler,不对运行的代码做任何额外的操作;若 NODE_ENV 的值不为 production,则返回一个新的 Profiler 对象,记录程序运行的时间。

参考资料

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