装饰器模式: 已是JS 的标准语法

164 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第15天,点击查看活动详情

装饰器模式定义

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。

例如,手机上套一个壳可以保护手机,壳上粘一个指环,可以挂在手指上不容易滑落,这就是一种装饰。手机还是那个手机,手机的功能一点都没变,只是在手机的外面装饰了一些其他附加的功能。日常生活中,这样的例子非常多。

function decorate(phone) {
    phone.fn3 = function () {
        console.log('指环')
    }
}
const phone = {
    name: 'iphone12',
    fn1() {}
    fn2() {}
}
const newPhone = decorate(phone)

而 ES 语法允许我们这样写(其实就是语法糖)

// 伪代码,不能运行
@decorate
const phone = { ... }

代码演示

class Circle {
  draw() {
    console.log('画一个圆')
  }
}

class Decorator {
  private circle: Circle
  constructor(circle: Circle) {
    this.circle = circle
  }
  draw() {
    this.circle.draw()
    this.setBorder()
  }
  private setBorder() {
    console.log('设置边框颜色')
  }
}

const circle = new Circle()
circle.draw()

const decorator = new Decorator(circle)
decorator.draw()

image.png

是否符合设计原则?

5 大设计原则中,最重要的就是:开放封闭原则,对扩展开放,对修改封闭。

  • 装饰器和目标分离,解耦
  • 装饰器可自行扩展
  • 目标也可自行扩展

装饰器的使用

ES 引入了 Decorator 语法,TS 也支持,但是需要在 tsconfig.json 中加 experimentalDecorators: true

装饰 class

// 装饰器
function testable(target: any) {
    // 当装饰类的时候 target就是类本身,即构造函数Foo
    target.isTestable = true
}

@testable
class Foo {
    static isTestable?: boolean
}

console.log(Foo.isTestable) // true

可以传入参数:

// 装饰器工厂函数
function testable(val: boolean) {
    // 装饰器
    return function (target: any) {
        target.isTestable = val
    }
}

@testable(false)
class Foo {
    static isTestable?: boolean
}

console.log(Foo.isTestable) // false

可以看到,装饰器本身就是一个函数,当传入参数的时候testable执行后也是返回一个函数。

装饰 class 方法

function readOnly(target: any, key: string, descriptor: PropertyDescriptor) {
    // 当装饰一个方法的时候target就是类的原型对象
    console.log('target', target)
    console.log('key', key)
    descriptor.writable = false
}

function configurable(val: boolean) {
    return function (target: any, key: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = val
    }
}

class Foo {
    private _name = '张三'
    private _age  = 20

    @readOnly
    getName() {
        return this._name
    }

    @configurable(false)
    getAge() {
        return this._age
    }
}

const f = new Foo()
// 给getName重新赋值报错
f.getName = () => { return 'hello' }

// { configurable: false }
console.log( Object.getOwnPropertyDescriptor(f.__proto__, 'getAge') ) 

react-redux使用装饰器

import { connect } from 'react-redux'

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

export default VisibleTodoList

如果使用装饰器就是

import { connect } from 'react-redux'

// 装饰器
@connect(mapStateToProps, mapDispatchToProps)
export default VisibleTodoList extends React.Component { }

AOP - Aspect Oriented Programming 面向切面编程

简单来说:业务和系统基础功能分离,用 Decorator 很合适。

image.png

可以把各个业务功能看出一条线,这条线在执行过程中会被日志,安全,鉴权等功能切一刀,这样就可以理解为面向切面编程。

AOP 和 OOP 并不冲突

在不使用装饰器的情况下,实现日志功能的时候,需要把日志功能嵌入到业务功能里面,这样就不符合软件开发原则了,都混在一块了。

class Foo {
    fn1() {
        // 打印日志
        log()
        console.log('业务功能1')
    }
    fn2() {
        // 打印日志
        log()
        console.log('业务功能2')
    }
}

所以,需要使用装饰器把日志功能单独抽离出来:

function log(target: any, key: string, descriptor: PropertyDescriptor) {
    const oldValue = descriptor.value // fn1 函数

    // 重新定义 fn1 函数
    descriptor.value = function () {
        console.log(`记录日志...`)
        return oldValue.apply(this, arguments)
    }
}

class Foo {
    @log // 不影响业务功能的代码,只是加了一个 log 的“切面”
    fn1() {
        console.log('业务功能1')
    }
}

const f = new Foo()
f.fn1()

这样就实现了业务功能和日志功能的分离解耦。