创建型(creational)设计模式,用于解决对象的创建问题。
- Factory(工厂)模式:把对象的创建逻辑封装成函数
- Revealing Constructor 模式:在创建对象的时候,把私有的属性与方法暂时暴露出来,并在创建完毕后隐藏
- Builder 模式:能够简化复杂对象的创建过程
- Singleton 模式与 Dependency Injection 模式:帮助开发者在应用程序中整理各模块之间的关系
Factory (工厂)模式
把对象的创建流程与该流程的实现方式解耦
工厂模式封装的是创建新实例的逻辑,让开发者能够在该模式内部,灵活地调控这个实例的创建手法。
可以在工厂里面选用 new 运算符创建某个类的实例,可以利用闭包动态构建一个有状态的对象字面量(stateful object literal),可以根据某项条件返回一种类型的对象。
工厂的使用者,不需要知道实例如何创建。
直接通过 new 创建对象,相当于把对象的创建方式定死了
export class Image {
constructor (path) {
this.path = path
}
}
import { Image } from './image.js'
export class ImageGif extends Image {
constructor (path) {
if (!path.match(/\.gif/)) {
throw new Error(`${path} is not a GIF image`)
}
super(path)
}
}
import { Image } from './image.js'
export class ImageJpeg extends Image {
constructor (path) {
if (!path.match(/\.jpe?g$/)) {
throw new Error(`${path} is not a JPEG image`)
}
super(path)
}
}
import { ImageGif } from './imageGif.js'
import { ImageJpeg } from './imageJpeg.js'
import { ImagePng } from './imagePng.js'
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')
}
}
const image1 = createImage('photo.jpg')
const image2 = createImage('photo.gif')
const image3 = createImage('photo.png')
console.log(image1, image2, image3)
强化封装效果
利用闭包,把工厂当作封装(encapsulation)机制使用
封装指的是控制组件的内部细节,让外部代码无法直接操纵这些细节。外界要想跟这个组件交互,只能通过公开的接口来做,这样可以让外部代码不会因为该组件的实现细节发生变化,而受到影响。
function createPerson (name) {
const privateProperties = {}
// 只能通过 person 对象提供的接口来操纵 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
}
const person = createPerson('James Joyce')
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
+`and ${diff[1]} nanoseconds.`)
} }
const noopProfiler = { start () {}, end () {} }
// 根据环境抛出不同实例的工厂函数 export function createProfiler (label) { if (process.env.NODE_ENV === ‘production’) { return noopProfiler }
return new Profiler(label) }
```typescript
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)
Builder (生成器/建造者)模式
- 主要目标:让开发者不用直接去调用复杂的构造器,而是在辅助方法的引导下,一步步构造对象,提高阅读性和便于管理
- 如果构造器的某几个参数具有联动关系,可以设计辅助方法来统一指定这些参数
- 如果某个参数的取值可以通过另一个参数推导出来,应该提供相关的 setter 方法(设置器方法),让开发者只需要指定后面那个参数就行,前面的那个参数的取值,放在 setter 方法内部去计算
如果有必要,在把相关的值传给要构建的那个类的构造器之前,多执行些处理(例如类型转换、正规化、或其他一些验证)
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
}
}
```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) } }
```typescript
import { UrlBuilder } from './urlBuilder.js'
const url = new UrlBuilder()
.setProtocol('https')
.setAuthentication('user', 'pass')
.setHostname('example.com')
.build()
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 }
// 判断该属性能够修改 buffer
if (MODIFIER_NAMES.some(m => prop.startsWith(m))) {
modifiers[prop] = buffer[prop].bind(buffer)
} else {
// 不能修改 buffer 的属性直接挂载到当前对象上
this[prop] = buffer[prop].bind(buffer)
}
}
// 让用户通过 modifiers 所存在的属性来修改 ImmutableBuffer 对象内部的 buffer
executor(modifiers)
} }
这个 ImmutableBuffer 是在使用它的人与它内部的那个普通 buffer 对象之间,充当代理。在 buffer 实例所能提供的方法里面,只读的那些方法,可以直接通过 ImmutableBuffer 接口调用,其他的可能修改内容的方法,则只能在 executor 函数里面使用
```typescript
import { ImmutableBuffer } from './immutableBuffer.js'
const hello = 'Hello!'
const immutable = new ImmutableBuffer(hello.length,
({ write }) => {
write(hello)
})
console.log(String.fromCharCode(immutable.readInt8(0)))
// the following line will throw
// "TypeError: immutable.write is not a function"
// immutable.write('Hello?')
revealing constructor 模式的大概模型如下:
const object = new SomeClass(function executor(revealedMembers) {
// 用户在这里编写代码,通过 revealedMembers 所展示的私密功能操纵本对象
})
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) {
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 Singleton class defines the
/**
The client code. */ function clientCode() { const s1 = Singleton.getInstance(); const s2 = Singleton.getInstance();
if (s1 === s2) {
console.log('Singleton works, both variables contain the same instance.');
} else {
console.log('Singleton failed, variables contain different instances.');
} }
clientCode();
Node.js 中的单例
```typescript
import { Database } from './Database.js'
export const dbInstance = new Database('my-app-db', {
url: 'localhost:5432',
username: 'user',
password: 'password'
})
无论哪个包引入 dbInstance 模块,使用的都是同一个实例。因为 Node.js 加载完某个模块后,会把它缓存起来,以后即使再次引入该模块,系统也只需要从缓存中取出这个模块,而不会重新将代码执行一遍,因此不用担心程序中会出现多个 Database 实例
管理模块之间的依赖关系
用 Singleton 模块管理模块之间的依赖关系
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import sqlite3 from 'sqlite3'
const __dirname = dirname(fileURLToPath(import.meta.url))
export const db = new sqlite3.Database(
join(__dirname, 'data.sqlite'))
import { promisify } from 'util'
import { db } from './db.js'
const dbRun = promisify(db.run.bind(db))
const dbAll = promisify(db.all.bind(db))
export class Blog {
initialize () {
const initQuery = `CREATE TABLE IF NOT EXISTS posts (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
return dbRun(initQuery)
}
createPost (id, title, content, createdAt) {
return dbRun('INSERT INTO posts VALUES (?, ?, ?, ?)',
id, title, content, createdAt)
}
getAllPosts () {
return dbAll('SELECT * FROM posts ORDER BY created_at DESC')
}
}
import { Blog } from './blog.js'
async function main () {
const blog = new Blog()
await blog.initialize()
const posts = await blog.getAllPosts()
if (posts.length === 0) {
console.log('No post available. Run `node import-posts.js`' +
' to load some sample posts')
}
for (const post of posts) {
console.log(post.title)
console.log('-'.repeat(post.title.length))
console.log(`Published on ${new Date(post.created_at)
.toISOString()}`)
console.log(post.content)
}
}
main().catch(console.error)
用 DI (依赖注入)管理模块之间的依赖关系
依赖注入(dependency injection,DI)模式,它让某个外部实体,把某组件所要依赖的东西输入给这个组件,这样的外部实体,通常称为 injector(注入者/注入方)。
injector 负责初始化各种组件,并把它们正确地联系起来。使用 injector 的主要优势,在于能够降低耦合程度,如果某个模块所要依赖的另外一个模块,我们不会把某个模块所要依赖的东西,直接写在该模块自己的代码里,而是由外界把这个东西传给该模块。
import { promisify } from 'util'
export class Blog {
constructor (db) {
this.db = db
this.dbRun = promisify(db.run.bind(db))
this.dbAll = promisify(db.all.bind(db))
}
initialize () {
const initQuery = `CREATE TABLE IF NOT EXISTS posts (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
return this.dbRun(initQuery)
}
createPost (id, title, content, createdAt) {
return this.dbRun('INSERT INTO posts VALUES (?, ?, ?, ?)',
id, title, content, createdAt)
}
getAllPosts () {
return this.dbAll(
'SELECT * FROM posts ORDER BY created_at DESC')
}
}
import sqlite3 from 'sqlite3'
export function createDb (dbFile) {
return new sqlite3.Database(dbFile)
}
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import { Blog } from './blog.js'
import { createDb } from './db.js'
const __dirname = dirname(fileURLToPath(import.meta.url))
async function main () {
// 数据库改由参数来进行构建
const db = createDb(join(__dirname, 'data.sqlite'))
// 注入我们自定义的数据库实例,将 blog 和 数据库彻底解耦
const blog = new Blog(db)
await blog.initialize()
const posts = await blog.getAllPosts()
if (posts.length === 0) {
console.log('No post available. Run `node import-posts.js`' +
' to load some sample posts')
}
for (const post of posts) {
console.log(post.title)
console.log('-'.repeat(post.title.length))
console.log(`Published on ${new Date(post.created_at)
.toISOString()}`)
console.log(post.content)
}
}
main().catch(console.error)