javascript 設計模式之裝飾者模式

文章系列

javascript 設計模式之單例模式javascript

javascript 設計模式之適配器模式java

javascript 設計模式之裝飾者模式git

javascript設計模式之代理模式github

javascript 適配、代理、裝飾者模式的比較web

javascript 設計模式之狀態模式npm

javascript 設計模式之迭代器模式編程

javascript 設計模式之策略模式設計模式

javascript 設計模式之觀察者模式安全

javascript 設計模式之發佈訂閱者模式babel

概念

裝飾者(decorator)模式,又名裝飾器模式,可以在不改變對象自身的基礎上,在程序運行期間給對像動態的添加職責。與繼承相比,裝飾者是一種更輕便靈活的作法。

本文代碼

就比如手機扣,有了手機扣會方便觀看視頻,但對於手機原先的全部功能,像是拍照仍然能夠直接使用。手機扣只是起到錦上添花做用,並不必定要有。

爲何要使用裝飾者模式

初始需求:

畫個圓

實現:

class Circle {
	draw() {
		console.info('畫圓')
	}
}
let c = new Circle()
c.draw()
複製代碼

更改需求:

畫個紅色的圓

實現:

或許這時你二話不說,就是找到 Circle 的 draw 方法改爲以下:

class Circle {
	draw() {
		console.info('畫圓')
		this.setRed()
	}
	setRed() {
		console.info('設置紅色邊框')
	}
}
let c = new Circle()
c.draw()
複製代碼

若是需求不改,這種實現方式倒也沒問題。 但若是哪天經理說我要實現個綠色邊框,是否是又得改爲:

class Circle {
	draw() {
		console.info('畫圓')
		this.setGreen()
	}
	setRed() {
		console.info('設置紅色邊框')
	}
	setGreen() {
		console.info('設置綠色邊框')
	}
}
let c = new Circle()
c.draw()
複製代碼

這種方式存在兩個問題:

  • Circle 這個類既處理了畫圓,又設置顏色,這違反了單一職責原則
  • 每次要設置不一樣顏色,都動到了 Circle 類,而且更改了 draw 方法,這違背了開放封閉原則(對添加開放,對修改封閉)

爲了新的業務需求不影響到原有功能,須要將舊邏輯與新邏輯分離:

class Circle {
	draw() {
		console.info('畫圓')
	}
}
class Decorator {
	constructor(circle) {
		this.circle = circle
	}
	draw() {
		this.circle.draw()
		this.setRedBorder()
	}
	setRedBorder() {
		console.info('設置紅色邊框')
	}
}
let c = new Circle()
let d = new Decorator(c)
d.draw()
複製代碼

此時若是要畫別的顏色圓形,只要修改 Decorator 類便可。

如此一來,就實現了"只添加,不修改原有的類"的裝飾者模式,使用 Decorator 的邏輯裝飾了舊的圓形邏輯。 固然你仍然能夠單純只建立個不帶顏色的圓形,用 c.draw() 便可

AOP 裝飾函數

AOP(Aspect Oriented Programming)面向切面編程。把一些與核心業務邏輯無關的功能抽離出來,再經過「動態織入」方式摻入業務邏輯模塊

與業務邏輯無關的功能一般包括日誌統計、安全控制、異常處理等等。

首先咱們要實現兩個函數:

一個用來前置裝飾,一個用來後置裝飾:

Function.prototype.before = function(beforeFunc){
    var that = this;
    return function(){
        beforeFunc.apply(this, arguments);
        return that.apply(this, arguments);
    }
}
Function.prototype.after = function(afterFunc){
    var that = this;
    return function(){
        var ret = that.apply(this, arguments);
        afterFunc.apply(this, arguments);
        return ret;
    }
}
複製代碼

之前置裝飾 before 爲例,調用 before 時,傳入一個函數,這個函數即爲新添加的函數,它裝載了新添加的功能代碼。

接着把當前的 this 保存起來,而後返回一個「代理」函數。這樣在原函數調用前,先執行擴展功能的函數,並且他們共用同一個參數列表。

後置裝飾與前置裝飾基本相似,只是執行順序不一樣

驗證:

var foobar = function (x, y, z) {
	console.log(x, y, z)
}
var foo = function (x, y, z) {
	console.log(x / 10, y / 10, z / 10)
}
var bar = function (x, y, z) {
	console.log(x * 10, y * 10, z * 10)
}
foobar = foobar.before(foo).after(bar)
foobar(1, 2, 3)
複製代碼

輸出:

0.1 0.2 0.3
1 2 3
10 20 30
複製代碼

以上設置 before 與 after 的方法污染了原型,能夠改爲:

var before = function (fn, beforeFunc) {
	return function () {
		beforeFunc.apply(this, arguments)
		return fn.apply(this, arguments)
	}
}
var after = function (fn, afterFunc) {
	return function () {
		var ret = fn.apply(this, arguments)
		afterFunc.apply(this, arguments)
		return ret
	}
}
var a = before(
	function () {
		alert(3)
	},
	function () {
		alert(2)
	}
)
a = before(a, function () {
	alert(1)
})
a()
複製代碼

輸出:

1
2
3
複製代碼

ES7 中的裝飾器

配置環境

  1. npm install babel-plugin-transform-decorators-legacy --save-dev
  2. 修改 .babelrc 文件:
{
    "presets":["es2015","latest"],
    "plugins": ["transform-decorators-legacy"] // 加上插件支持 ES7 的裝飾語法
}
複製代碼

裝飾類

裝飾類不帶參數

// 裝飾器函數,它的第一個參數是目標類
function classDecorator(target) {
	target.hasAdd = true
	// return target // 無關緊要, 默認就是返回 this 的
}

// 將裝飾器"安裝"到 Button 類上
@classDecorator
class Button {}

// 驗證裝飾器是否生效
alert('Button 是否被裝飾了:' + Button.hasAdd)
複製代碼

等價於

function classDecorator(target) {
	target.hasAdd = true
	return target // 此時必定要用, 由於這時是做爲函數使用,而非構造函數
}

class Button {}

Button = classDecorator(Button)
// 驗證裝飾器是否生效
alert('Button 是否被裝飾了:' + Button.hasAdd)
複製代碼

說明裝飾器的原理:

@decorator
class A{}

//等同於
A = decorator(A) || A
複製代碼

代表 ES7 中的裝飾器也是個語法糖

裝飾類帶參數

// 裝飾器要接收參數時,就要返回個函數,該函數的第一個參數是目標類
function classDecorator(name) {
	return function (target) {
		target.btnName = name
	}
}

// 將裝飾器"安裝"到 Button 類上
@classDecorator('登陸')
class Button {}

// 驗證裝飾器是否生效
alert('按鈕名稱:' + Button.btnName)

複製代碼

等同於

// 裝飾器要接收參數時,就要返回個函數,該函數的第一個參數是目標類
function classDecorator(name) {
	return function (target) {
		target.btnName = name
		return target
	}
}

// 將裝飾器"安裝"到 Button 類上
class Button {}

Button = classDecorator('登陸')(Button)
// 驗證裝飾器是否生效
alert('按鈕名稱:' + Button.btnName)
複製代碼

裝飾類 - mixin 示例

function mixin(...list) {
	console.info(...list, 'list') 
        // ...list 是個對象, key 爲 "foo",值爲 function() { alert('foo')}
	return function (target) {
		Object.assign(target.prototype, ...list)
		console.dir(target, 'target')
	}
}
const Foo = {
	foo() {
		alert('foo')
	}
}
@mixin(Foo)
class Button {}
let d = new Button()
d.foo()
複製代碼

輸出

能夠看到是往 Button 類的原型上加上了 foo 函數,那可能有人會問了,爲何不在類上直接加呢,即

function mixin(...list) {
	console.info(...list, 'list') // ...list 是個對象, key 爲 "foo",值爲 function() { alert('foo')}
	return function (target) {
		Object.assign(target, ...list)
		console.dir(target, 'target')
	}
}
const Foo = {
	foo() {
		alert('foo')
	}
}
@mixin(Foo)
class Button {}
let d = new Button()
d.foo()
複製代碼

此時會報Uncaught TypeError: d.foo is not a function 錯誤

這是因爲實例是在代碼運行時動態生成的,而裝飾器函數則是在編譯階段就執行了,因此裝飾器 mixin 函數執行時, Button 實例還不存在。

爲了確保實例生成後能夠順利訪問到被裝飾好的方法(foo),裝飾器只能去修飾 Button 類的原型對象。

裝飾方法

readonly 示例

function readonly(target, name, descriptor) {
	descriptor.writable = false
	return descriptor
}

class Person {
	constructor() {
		this.first = 'A'
		this.last = 'B'
	}

	@readonly
	name() {
		return `${this.first} ${this.last}`
	}
}

let p = new Person()
console.info(p.name())
// p.name = function () {
// console.info(100)
// } // 修改會報錯

複製代碼

log 示例

function log(target, name, descriptor) {
	let oldValue = descriptor.value
	descriptor.value = function () {
		console.log(`calling ${name} width`, arguments)
		return oldValue.apply(this, arguments)
	}
	return descriptor
}

class Math {
	@log
	add(a, b) {
		return a + b
	}
}
let math = new Math()
const result = math.add(4, 6)
alert(result)

複製代碼

裝飾方法總結

以上 readonly 與 log 都是經過修改 descriptor 實現的,那該裝飾方法的函數的三個參數都分別表示什麼呢?

目前有個開源的第三方庫 core-decorators,提供了不少好用的裝飾方法。

// import { readonly } from 'core-decorators'

// class Person {
// @readonly
// name() {
// return 'zhang'
// }
// }

// let p = new Person()
// alert(p.name())
// // p.name = function () { /*...*/ } // 此處會報錯

import { deprecate } from 'core-decorators'

class Person {
	@deprecate
	facepalm() {}

	@deprecate('We stopped facepalming')
	facepalmHard() {}

	@deprecate('We stopped facepalming', {
		url: 'http://knowyourmeme.com/memes/facepalm'
	})
	facepalmHarder() {}
}

let person = new Person()

person.facepalm()
// DEPRECATION Person#facepalm: This function will be removed in future versions.

person.facepalmHard()
// DEPRECATION Person#facepalmHard: We stopped facepalming

person.facepalmHarder()
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
// See http://knowyourmeme.com/memes/facepalm for more details.

複製代碼

參考連接

JavaScript 設計模式核⼼原理與應⽤實踐

結語

你的點贊是對我最大的確定,若是以爲有幫助,請留下你的讚揚,謝謝!!!

相關文章
相關標籤/搜索