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 的逻辑装饰了旧的圆形逻辑。
当然你仍然可以单纯只创建个不带颜色的圆形,用 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.prototype, ...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.

装饰者模式使用场景

React 高阶函数

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

需求:
实现"只有在登录时才显示" 这个功能,比如只有在登录时才显示退出登录按钮,或者只有在登录时才显示购物车

const LogoutButton = () => {
	if (getUserId()) {
		return '...' // 显示"退出登录"的JSX
	} else {
		return null
	}
}
// 购物车
const ShoppingCart = () => {
	if (getUserId()) {
		return '...' // 显示"购物车"的JSX
	} else {
		return null
	}
}

存在如下问题:

  1. 代码冗余, 两个组件明显有重复代码
  2. 违反单一职责,LogoutButton 应该只负责 退出登录相关的业务,而不应该包含判断是否登录逻辑

采用高级组件修复以上问题

const withLogin = (Component) => {
	const NewComponent = (props) => {
		if (getUserId()) {
			return <Component {...props} />
		} else {
			return null
		}
	}
	return NewComponent
}

const LoginButton = withLogin((props) => {
	return '...' // 显示"退出登录"的JSX
})

const ShoppingCart = withLogin((props) => {
	return '...' // 显示"购物车"的JSX
})

可以看出,高阶组件从实现层面上来看其实就是上文我们提到的类装饰器。

改写 Redux connect

在 React 中,当我们想要引入 Redux 时,通常需要调用 connect 方法来把状态和组件绑在一起:

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import action from './action.js'

class App extends Component {
  render() {
    // App的业务逻辑
  }
}

function mapStateToProps(state) {
  return state.app
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(action, dispatch)
}

// 把App组件与Redux绑在一起
export default connect(mapStateToProps, mapDispatchToProps)(App)

调用 connect 可以返回一个具有装饰作用的函数,这个函数可以接收 React 组件作为参数,使这个目标组件和 Redux 结合、具备 Redux 提供的数据和能力。既然有装饰作用,既然是能力的拓展,那么就一定能用装饰器来改写:
先把 connect 抽出来:

import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import action from './action.js'

function mapStateToProps(state) {
  return state.app
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(action, dispatch)
}

// 将connect调用后的结果作为一个装饰器导出
export default connect(mapStateToProps, mapDispatchToProps)

组件里要使用 redux, 只要引入该封装的 connect 函数即可

import React, { Component } from 'react'
import connect from './connect.js'   

@connect
export default class App extends Component {
  render() {
    // App的业务逻辑
  }
}

可以看出代码清爽了很多

参考链接

JavaScript 设计模式核⼼原理与应⽤实践

结语

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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值