提供一种管理实体关系的方式
- Proxy(代理):让我们能够在用户访问另一个对象时做出管控
- Decorator(修饰器):能够动态地增强现有对象的行为
Adapter(适配器):让我们能够访问到与某对象接口不同的另一个对象所具备的功能
Proxy(代理)模式
当用户(Client)通过 proxy 来操作 subject 的时候,proxy 会把用户想要对 subject 所执行的全部操作(或其中某些操作)拦截下来,以便增强或补充该操作的功能。
proxy 对象与 subject 对象的接口是相同的,两者是可以互换的。
可以在以下情况使用:验证数据
- 确保安全:是否有权限执行操作
- 实现缓存机制
- 惰性初始化:如果 subject 对象创建起来开销比较大,那就让 proxy 尽量推迟 subject 的创建时机,只有在必须用到 subject 的时候,才去真正创建它
- 实现日志记录功能
-
怎样实现 Proxy 模式
```typescript class StackCalculator { constructor () { this.stack = [] }
putValue (value) { this.stack.push(value) }
getValue () { return this.stack.pop() }
peekValue () { return this.stack[this.stack.length - 1] }
clear () { this.stack = [] }
divide () { const divisor = this.getValue() const dividend = this.getValue() const result = dividend / divisor this.putValue(result) return result }
multiply () { const multiplicand = this.getValue() const multiplier = this.getValue() const result = multiplier * multiplicand this.putValue(result) return result } }
<a name="rwM7K"></a>### 通过组合对象来实现 Proxy 模式> 组合(composition)是把某对象与另一个对象相结合,让后者能够扩充或使用前者的功能。Proxy 模式中,是创建一个新的对象(proxy 对象),让它的接口跟原对象(subject 对象)相同,并且让 proxy 对象通过实例变量或闭包变量来保存 subject,可在创建 proxy 对象时注入,或由 proxy 自己创建。```typescriptclass SafeCalculator {constructor (calculator) {this.calculator = calculator}// proxied methoddivide () {// additional validation logicconst divisor = this.calculator.peekValue()if (divisor === 0) {throw Error('Division by 0')}// if valid delegates to the subjectreturn this.calculator.divide()}// delegated methodsputValue (value) {return this.calculator.putValue(value)}getValue () {return this.calculator.getValue()}peekValue () {return this.calculator.peekValue()}clear () {return this.calculator.clear()}multiply () {return this.calculator.multiply()}}const calculator = new StackCalculator()const safeCalculator = new SafeCalculator(calculator)calculator.putValue(3)calculator.putValue(2)console.log(calculator.multiply()) // 3*2 = 6safeCalculator.putValue(2)console.log(safeCalculator.multiply()) // 6*2 = 12calculator.putValue(0)console.log(calculator.divide()) // 12/0 = InfinitysafeCalculator.clear()safeCalculator.putValue(4)safeCalculator.putValue(0)console.log(safeCalculator.divide()) // 4/0 -> Error('Division by 0')
通过工厂函数与对象字面量来创建 proxy 对象
function createSafeCalculator (calculator) {return {// proxied methoddivide () {// additional validation logicconst divisor = calculator.peekValue()if (divisor === 0) {throw Error('Division by 0')}// if valid delegates to the subjectreturn calculator.divide()},// delegated methodsputValue (value) {return calculator.putValue(value)},getValue () {return calculator.getValue()},peekValue () {return calculator.peekValue()},clear () {return calculator.clear()},multiply () {return calculator.multiply()}}}const calculator = new StackCalculator()const safeCalculator = createSafeCalculator(calculator)
通过增强原对象来实现 Proxy 模式
在原对象只有少数几个方法需要代理时,通过直接修改 subject,替换相应的方法为我们实现的代理方法
function patchToSafeCalculator (calculator) {const divideOrig = calculator.dividecalculator.divide = () => {// additional validation logicconst divisor = calculator.peekValue()if (divisor === 0) {throw Error('Division by 0')}// if valid delegates to the subjectreturn divideOrig.apply(calculator)}return calculator}
用内置的 Proxy 对象实现 Proxy 模式
const proxy = new Proxy(target, handler);
const safeCalculatorHandler = {get: (target, property) => {if (property === 'divide') {// proxied methodreturn function () {// additional validation logicconst divisor = target.peekValue()if (divisor === 0) {throw Error('Division by 0')}// if valid delegates to the subjectreturn target.divide()}}// delegated methods and propertiesreturn target[property]}}const calculator = new StackCalculator()const safeCalculator = new Proxy(calculator, safeCalculatorHandler)
用内置的 Proxy 做出来的这种对象,会继承 subject 的 prototype(原型),因此,判断
safeCalculator instanceof StackCalculator所得到的结果是 true
创建带有日志功能的 Writable 流
export function createLoggingWritable (writable) {return new Proxy(writable, { // ①get (target, propKey, receiver) { // ②// 判断受访问属性if (propKey === 'write') { // ③return function (...args) { // ④const [chunk] = argsconsole.log('Writing', chunk)return writable.write(...args)}}return target[propKey] // ⑤}})}
import { createWriteStream } from 'fs'import { createLoggingWritable } from './logging-writable.js'const writable = createWriteStream('test.txt')const writableProxy = createLoggingWritable(writable)writableProxy.write('First chunk')writableProxy.write('Second chunk')writable.write('This is not logged')writableProxy.end()
用 Proxy 实现 Change Observer 模式
Change Observer 模式让某对象(subject)把状态的变化情况通知一个或多个 observe(观察者),令它们能够在 subject 的状态发生改变时尽快做出反应。
和 Observer 模式的区别:
- Change Observer 的重点在于让我们能够监测对象属性上面的变化
Observer 模式采用 EventEmitter 播报信息,让我们知道系统里面发生了什么事件,这个事件不一定是属性上的变化
export function createObservable (target, observer) {const observable = new Proxy(target, {set (obj, prop, value) {if (value !== obj[prop]) {const prev = obj[prop]obj[prop] = value// 对象发生变化时触发的函数observer({ prop, prev, curr: value })}return true}})return observable}
```typescript import { createObservable } from ‘./create-observable.js’
function calculateTotal (invoice) { // ① return invoice.subtotal - invoice.discount + invoice.tax }
const invoice = {
subtotal: 100,
discount: 10,
tax: 20
}
let total = calculateTotal(invoice)
console.log(Starting total: ${total})
const obsInvoice = createObservable( // ②
invoice,
({ prop, prev, curr }) => {
total = calculateTotal(invoice)
console.log(TOTAL: ${total} (${prop} changed: ${prev} -> ${curr}))
}
)
// ③ obsInvoice.subtotal = 200 // TOTAL: 210 obsInvoice.discount = 20 // TOTAL: 200 obsInvoice.discount = 20 // no change: doesn’t notify obsInvoice.tax = 30 // TOTAL: 210
console.log(Final total: ${total})
<a name="GIML7"></a># Decorator(修饰器)模式> 能够动态地扩充现有对象的行为。主要是给其中添加新的功能。<a name="H0mt0"></a>## 实现 Decorator 模式的几种办法<a name="JE4XN"></a>### 用组合实现 Decorator 模式把受装饰的组件包裹在一个新的对象里面,并让这个新的对象所在的类,支持原组件所具备的那套功能。```typescriptclass EnhancedCalculator {constructor (calculator) {this.calculator = calculator}// new methodadd () {const addend2 = this.getValue()const addend1 = this.getValue()const result = addend1 + addend2this.putValue(result)return result}// modified methoddivide () {// additional validation logicconst divisor = this.calculator.peekValue()if (divisor === 0) {throw Error('Division by 0')}// if valid delegates to the subjectreturn this.calculator.divide()}// ...}
通过增强原对象来实现 Decorator 模式
把新方法直接添加到受修饰的对象里面
function patchCalculator (calculator) {// new methodcalculator.add = function () {const addend2 = calculator.getValue()const addend1 = calculator.getValue()const result = addend1 + addend2calculator.putValue(result)return result}// modified methodconst divideOrig = calculator.dividecalculator.divide = () => {// additional validation logicconst divisor = calculator.peekValue()if (divisor === 0) {throw Error('Division by 0')}// if valid delegates to the subjectreturn divideOrig.apply(calculator)}return calculator}const calculator = new StackCalculator()const enhancedCalculator = patchCalculator(calculator)
通过 Proxy 对象实现 Decorator 模式
const enhancedCalculatorHandler = {get (target, property) {if (property === 'add') {// new methodreturn function add () {const addend2 = target.getValue()const addend1 = target.getValue()const result = addend1 + addend2target.putValue(result)return result}} else if (property === 'divide') {// modified methodreturn function () {// additional validation logicconst divisor = target.peekValue()if (divisor === 0) {throw Error('Division by 0')}// if valid delegates to the subjectreturn target.divide()}}// delegated methods and propertiesreturn target[property]}}const calculator = new StackCalculator()const enhancedCalculator = new Proxy(calculator, enhancedCalculatorHandler)
使用 Decorator 模式来修饰 LevelUP 数据库
export function levelSubscribe (db) {db.subscribe = (pattern, listener) => { // ①db.on('put', (key, val) => { // ②const match = Object.keys(pattern).every(k => (pattern[k] === val[k]) // ③)if (match) {listener(key, val) // ④}})}return db}
import { dirname, join } from 'path'import { fileURLToPath } from 'url'import level from 'level'import { levelSubscribe } from './level-subscribe.js'const __dirname = dirname(fileURLToPath(import.meta.url))const dbPath = join(__dirname, 'db')const db = level(dbPath, { valueEncoding: 'json' }) // ①levelSubscribe(db) // ②db.subscribe( // ③{ doctype: 'tweet', language: 'en' },(k, val) => console.log(val))db.put('1', { // ④doctype: 'tweet',text: 'Hi',language: 'en'})db.put('2', {doctype: 'company',name: 'ACME Co.'})
Adapter(适配器)模式
Adapter 模式让我们能够通过一套与原对象不同的接口来访问原对象的功能
通过 fs 式的 API 来使用 LevelUP
构建一个针对 LevelUP API 的 adapter (适配器),让它把那套接口转换成一套与 fs 这个核心模块相兼容的接口。
保证程序每次在 adapter 上面调用 readFile() 与 writeFile() 时,能够把这些操作转换为 db.get() 与 db.put() 操作。
能够让 LevelUP 数据库充当存储后端(storage backend),并利用适配器将其转化成一种支持 fs 接口的对象,使得程序能够在这个对象上面,通过简单的文件操作来修改后端数据库中的内容。
import { resolve } from 'path'export function createFSAdapter (db) {return ({// 确保该函数的接口与 fs 模块所设计的同名函数相兼容readFile (filename, options, callback) {if (typeof options === 'function') {callback = optionsoptions = {}} else if (typeof options === 'string') {options = { encoding: options }}db.get(resolve(filename), { // ①valueEncoding: options.encoding},(err, value) => {if (err) {if (err.type === 'NotFoundError') { // ②err = new Error(`ENOENT, open "${filename}"`)err.code = 'ENOENT'err.errno = 34err.path = filename}return callback && callback(err)}callback && callback(null, value) // ③})},writeFile (filename, contents, options, callback) {if (typeof options === 'function') {callback = optionsoptions = {}} else if (typeof options === 'string') {options = { encoding: options }}db.put(resolve(filename), contents, {valueEncoding: options.encoding}, callback)}})}
import { dirname, join } from 'path'import { fileURLToPath } from 'url'import level from 'level'import { createFSAdapter } from './fs-adapter.js'const __dirname = dirname(fileURLToPath(import.meta.url))const db = level(join(__dirname, 'db'), {valueEncoding: 'binary'})const fs = createFSAdapter(db)fs.writeFile('file.txt', 'Hello!', () => {fs.readFile('file.txt', { encoding: 'utf8' }, (err, res) => {if (err) {return console.error(err)}console.log(res)})})// try to read a missing filefs.readFile('missing.txt', { encoding: 'utf8' }, (err, res) => {console.error(err)})
可以用类似的技术,让某些使用 fs 式 API 的代码,能够同时在 Node.js 平台与浏览器平台运行。
