工厂(Factory)模式 是 Node.js 中最常见的设计模式之一。
其具有以下优势:
- 将对象的创建过程与对象的实现细节进行解耦。由工厂创建一系列对象,某个对象继承的特征在运行时确定
- 工厂模式允许我们对外暴露更少的接口。一个类可以被扩展或者操控,而工厂本身仅仅是一个负责创建对象的函数,没有给用户其他选项,从而使接口更健壮和容易理解
- 借助闭包可以帮助强化对象的封装
解耦对象的创建和实现
工厂模式封装了新对象的创建过程,给这个过程提供了更多的灵活性和控制。在工厂内部我们可以选择各种不同的方式来创建某个对象的实例,工厂的消费者对于这些细节一无所知。
相反地,使用 new
关键字则会将代码绑定到一种特定的创建方式上。
比如下面的一个用于创建 Image
对象的工厂函数:1
2
3
4function 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
11function 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
25function 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
对象通过其 setName
和 getName
方法访问。
但与此同时,该 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
28class 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
23import { 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
对象,记录程序运行的时间。