javascript 设计模式之装饰者模式

2,359 阅读6分钟

文章系列

javascript 设计模式之单例模式

javascript 设计模式之适配器模式

javascript 设计模式之装饰者模式

javascript设计模式之代理模式

javascript 适配、代理、装饰者模式的比较

javascript 设计模式之状态模式

javascript 设计模式之迭代器模式

javascript 设计模式之策略模式

javascript 设计模式之观察者模式

javascript 设计模式之发布订阅者模式

概念

装饰者(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 设计模式核⼼原理与应⽤实践

结语

你的点赞是对我最大的肯定,如果觉得有帮助,请留下你的赞赏,谢谢!!!