Node.js 设计模式笔记 —— Builder 模式

Builder 是一种创建型设计模式,可以通过提供简单平滑的接口来简化复杂对象的创建,允许我们一步一步的构建新对象。
最明显的需要使用 Builder 模式的时候,就是当某个类的构造函数包含了太多的参数。

比如下面的一个 Boat 类:

1
2
3
4
5
6
7
class Boat {
constructor(hasMotor, motorCount, motorBrand, motorModel,
hasSails, sailsCount, sailsMaterial, sailsColor,
hullColor, hasCabin) {
// ...
}
}

调用上述 Boat 类的构造方法会导致出现非常难以阅读的代码:

1
2
const myBoat = new Boat(true, 2, 'Best Motor Co. ', 'OM123', true, 1,
'fabric', 'white', 'blue', false)

想要提升上述构造函数的设计,首先可以将所有的参数整合到一个单一的对象中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Boat {
constructor(allParameters) {
// ...
}
}

const myBoat = new Boat({
hasMotor: true,
motorCount: 2,
motorBrand: 'Best Motor Co. ',
motorModel: 'OM123',
hasSails: true,
sailsCount: 1,
sailsMaterial: 'fabric',
sailsColor: 'white',
hullColor: 'blue',
hasCabin: false,
})

新版本的构造函数跟原来相比提升了不少,比如用户可以清晰地看到每个传入的参数所代表的具体含义。
但是,将所有参数都放入同一个对象后再传入构造函数的方式,也有其自身的缺点。比如要想知道每个参数的具体含义,还是需要查看类的说明文档甚至类的代码。此外,没有任何强制性的协议来引导用户创建一致的对象,假如我们指定 hasMotor: true,意味着我们同时还需要再指定 motorCountmotorBrandmotorModel 参数的值。但我们无从获取此类信息(除非查看源代码)。

Builder 模式就非常适合解决上述问题。帮助用户创建一个平滑、易读、自说明的生成对象的接口,同时为创建具有一致性的对象提供指导信息。

使用 Builder 模式的 Boat 类:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Boat {
constructor(allParameters) {
// ...
}
}

class BoatBuilder {
withMotors(count, brand, model) {
this.hasMotor = true
this.motorCount = count
this.motorBrand = brand
this.motorModel = model
return this
}

withSails(count, material, color) {
this.hasSails = true
this.sailsCount = count
this.sailsMaterial = material
this.sailsColor = color
return this
}

hullColor(color) {
this.hullColor = color
return this
}

withCabin() {
this.hasCabin = true
return this
}

build() {
return new Boat({
hasMotor: this.hasMotor,
motorCount: this.motorCount,
motorBrand: this.motorBrand,
motorModel: this.motorModel,
hasSails: this.hasSails,
sailsCount: this.sailsCount,
sailsMaterial: this.sailsMaterial,
sailsColor: this.sailsColor,
hullColor: this.hullColor,
hasCabin: this.hasCabin
})
}
}

const myBoat = new BoatBuilder()
.withMotors(2, 'Best Motor Co. ', 'OM123')
.withSails(1, 'fabric', 'white')
.withCabin()
.hullColor('blue')
.build()

BoatBuilder 类的作用就是收集 Boat 类需要的所有参数,再通过一系列 helper 方法传递给 Boat

Builder 模式的基本规则:

  • 将主要对象的复杂构建过程拆分为几个更为易读的、更容易管理的步骤
  • 尝试创建 builder 方法,向需要创建的对象一组一组地传递相关联的参数
  • 必要的情况下,在通过 builder 方法将参数传递给需要创建的对象前,尽可能地先对参数做一些处理

实例:URL builder

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

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

url.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
export class Url {
constructor(protocol, username, password, hostname,
port, pathname, search, hash) {
this.protocol = protocol
this.username = username
this.password = password
this.hostname = hostname
this.port = port
this.pathname = pathname
this.search = search
this.hash = hash

this.validate()
}

validate() {
if (!this.protocol || !this.hostname) {
throw new Error('Must specify at least a ' + 'protocol and a hostname')
}
}

toString() {
let url = ''
url += `${this.protocol}://`
if (this.username && this.password) {
url += `${this.username}:${this.password}@`
}
url += this.hostname
if (this.port) {
url += this.port
}
if (this.pathname) {
url += this.pathname
}
if (this.search) {
url += `?${this.search}`
}
if (this.hash) {
url += `#${this.hash}`
}
return url
}
}

urlBuilder.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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)
}
}

index.js

1
2
3
4
5
6
7
8
9
import {UrlBuilder} from './urlBuilder.js'

const url = new UrlBuilder()
.setProtocol('https')
.setAuthentication('user', 'pass')
.setHostname('example.com')
.build()

console.log(url.toString())

运行效果:

1
2
$ node index.js
https://user:pass@example.com

参考资料

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