创建型(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) }
```typescriptimport { 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 = protocolthis.username = usernamethis.password = passwordthis.hostname = hostnamethis.port = portthis.pathname = pathnamethis.search = searchthis.hash = hashthis.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.hostnameif (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) } }
```typescriptimport { 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 }
// 判断该属性能够修改 bufferif (MODIFIER_NAMES.some(m => prop.startsWith(m))) {modifiers[prop] = buffer[prop].bind(buffer)} else {// 不能修改 buffer 的属性直接挂载到当前对象上this[prop] = buffer[prop].bind(buffer)}}// 让用户通过 modifiers 所存在的属性来修改 ImmutableBuffer 对象内部的 bufferexecutor(modifiers)
} }
这个 ImmutableBuffer 是在使用它的人与它内部的那个普通 buffer 对象之间,充当代理。在 buffer 实例所能提供的方法里面,只读的那些方法,可以直接通过 ImmutableBuffer 接口调用,其他的可能修改内容的方法,则只能在 executor 函数里面使用```typescriptimport { 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
getInstancemethod 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
newoperator. */ 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 中的单例```typescriptimport { 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 = dbthis.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)
