设计模式 - 装饰器模式和适配器模式

前置解析

平时开发一个项目时,如果想要拓展一个对象或函数(函数本质上也是对象),可以直接修改原来的对象使其拥有新的功能,但是这样做是违背了开放封闭原则单一职责原则的,而且随着业务发展就会变得越来越难以维护。装饰着模式就是用来解决这种问题的,它会将新添加的功能定义为新的装饰类,然后使用装饰类来装饰(包装)一下原有的对象,使得原有对象可以轻易拥有装饰类的功能且自身不会改变。
装饰者模式就是动态的给或者对象添加职责的设计模式。可以在不改变类或对象自身的基础上,在运行期间动态的添加职责,这种设计模式非常符合敏捷开发的设计思想:先提炼出产品的MVP(Minimun Viable Product:最小可用产品),再通过快速迭代的方式添加功能;常用来实现面向切面编程(AOP)和链式调用,以及对现有类的功能增强、聚合、缓存等实现
传统的装饰者模式需要将原有的逻辑缓存下来,然后在添加新功能时提前执行之前逻辑,然后再执行新功能的方式,此过程需要注意返回值和this指向的问题

  • 使用场景分析
    • 对象和类动态的拓展功能
      • 想要对已有业务的迭代,就需要将旧的逻辑与新逻辑分离,将旧的逻辑抽出去,实现隔离的作用
      • 通过装饰器来包装对象,拓展对象的功能,可以选择性的添加想要的功能,更加灵活
    • 组件聚合
      • 将多个简单的组件组合成一个复合组件,达到复用、封装的目的
    • 缓存
      • 当缓存的数据不存在时,可以通过装饰器来尝试从其他源获取并缓存下来
  • 优缺点分析
    • 优点
      • 可以动态的给原型对象添加功能,非常灵活
      • 添加新功能的同时不会影响原对象,符合开放封闭原则
      • 装饰对象与原对象解耦,易于维护
    • 缺点
      • 定义过多的装饰类,增加系统的复杂性

深入分析内部原理

定义:在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂的需求;其优点就是无侵入性,不破坏原有代码,只是在原有代码的基础上增加功能

实现方式

实现装饰器模式,需要维护目标对象的一个引用,同时要实现目标类的所有接口,调用方法时,需要先执行目标对象原有的方法,再执行自行添加的特性;

一般当接口比较多,装饰器也比较多时,可以独立抽取一个装饰器父类,实现目标类的所有接口,再创建真正的装饰器来继承父类

ES6的方式实现
js 复制代码
class Person {
  // 原始类方法
  constructor(name) {
    this.name = name;
  }

  say() {
    console.log(`I am ${this.name}.`);
  }
}

class Decorator {
  // 实现装饰器基类 内部依赖原始类方法 用于继承原始类方法上的属性配置 自生也可以直接被实例化使用
  constructor(person) {
    this.person = person;
  }

  say() {
    this.person.say();
  }
}

class Programmer extends Decorator {
  // 通过继承的方式实现对原始基类的相关拓展,并且保证原始逻辑的正常执行
  constructor(person) {
    super(person);
  }

  say() {
    this.person.say();
    console.log(`I am a programmer.`);
  }
  
  sayOwn() {
    this.say();
    console.log(`I am a extends programmer.`);
  }
}

let person = new Person('Tom');
let programmer = new Programmer(person);
programmer.say(); // I am Tom. I am a programmer.
AOP方式实现

AOP(Aspect-Oriented Programming:切面编程)是一种程序设计方法,用于将交叉性的关注点分离出来,采用AOP可以提升系统的可维护性和可拓展性,避免代码冗余和具有模块化能力;

  • AOP的实现方式

    • 静态AOP

      • 是在编译时在编译器层面进行注入的操作,将AOP功能与业务代码静态注入,生成新的类并编译成class文件,进而去运行。
    • 动态AOP

      • 是在程序运行中进行的操作,不需要事先对源码进行修改,而是在运行时通过动态代理技术组织AOP功能,运行时反射获取目标类中的方法,以此构造出切面代码并将其组织到运行时环境
      js 复制代码
      // 依赖ES7中的descriptor语法
      function log(target, name, descriptor) {
        const original = descriptor.value; //原始方法缓存
        descriptor.value = function (...args) { // 重新定义
          console.log(`Method ${name} called with parameters ${args.join(', ')}.`); // 新添加的方法
          const result = original.apply(this, args); // 通过缓存函数执行原始逻辑
          console.log(`Method ${name} returns ${result}.`); //新添加的方法
          return result; // 返回原始方法的执行结果到descriptor的value中,以便后续的原始数据获取
        };
        return descriptor; // 返回descriptor 以便在使用时检查元数据
      }
      
      class Calculator {
        @log
        add(x, y) {
          return x + y;
        }
      }
      
      const calc = new Calculator();
      console.log(calc.add(2, 3)); // Method add called with parameters 2, 3. Method add returns 5. 5
  • AOP的应用场景

    • 日志记录
      • 通过在函数执行前后加入日志代码,可以追踪程序的运行情况,快速定位问题
    • 认证和授权
      • 通过在函数执行前检查用户权限,可以确保用户有权限访问,用于保障系统安全
    • 监控和性能统计
      • 通过在函数执行前后加入监控代码,可以统计函数的执行次数、时长等信息,用于后续的优化系统性能
    • 权限校验和缓存
  • AOP实现装饰器模式

使用AOP对函数进行装饰,使得原函数在执行前后添加新功能,并且不改变原函数的自身代码;下列方式会影响到原函数上的属性,因为调用after和before是返回了一个新函数

js 复制代码
    const before = Symbol('before');
    const after = Symbol('before');

    // 定义AOP装饰函数
    Function.prototype[before] = function (beforeFn) {
      const _self = this; // 保存原函数引用

      // 负责函数执行顺序
      // 返回包含了原函数和新函数的「代理」函数
      return function (...params) {
        beforeFn.apply(this, params); // 插入之前函数执行 保证this不被劫持
        _self.apply(this, params); // 执行原函数
      };
    };

    // 定义AOP装饰函数
    Function.prototype[after] = function (afterFn) {
      const _self = this; // 保存原函数引用

      // 负责函数执行顺序
      return function (...params) {
        _self.apply(this, params); // 执行原函数
        afterFn.apply(this, params); // 插入之后函数执行
      };
    };

    let eat = function () {
      console.log('好好吃饭长高高');
    };

    const wash = function () {
      console.log('必须先洗手,不然不给吃饭');
    };

    const play = function () {
      console.log('终于吃完了,我要去玩玩玩玩');
    };

    eat = eat[before](wash)[after](play);
    eat();

    // 打印
    // 必须先洗手,不然不给吃饭
    // 好好吃饭长高高
    // 终于吃完了,我要去玩玩玩玩
高阶函数方式实现
js 复制代码
// fn为原函数,beforefn为装饰函数, 生成的装饰后的函数其内部返回原函数执行后的值
// 前置装饰
const before = (fn, beforefn) => (...args) => {
    console.log(args) // [11, 123]
  beforefn.apply(this,args);
  return fn.apply(this,args);
}
// 后置装饰
const after  = (fn, afterfn) => (...args) => {
    console.log(args,112) // [22, 321] 112
  const res = fn.apply(this,args);
  afterfn.apply(this,args);
  return res;
}

const test = () => console.log(0);
const test1 = () => console.log(1);
const test2 = () => console.log(2);
before(before(test, test1), test2)(11,123);
after(after(test, test1), test2)(22,321);

拓展

适配器模式(Adapter Pattern) → 核心思想是封装旧接口,暴露新接口

适配器模式就是通过将一个类的接口变换成客户端所期待的另一种接口,从而解决不兼容的问题
适配器是一种结构型设计模式,允许将不兼容的对象包装成一个兼容的接口,从而使得他们可以在一起工作,其主要需要做的是将转化留给自己,把统一留给用于

适配器模式通常包含三个角色:

  • 客户端、目标对象和适配器对象;
    • 客户端(目标接口)提供客户端需要的接口,
    • 适配器对象(充当中间人角色),调用目标对象的接口,将目标对象的接口转换为客户端需要的接口,从而实现兼容性;
    • 被适配者:提供需要被转换的接口
  • 优缺点分析
    • 优点:
      • 提高代码的可复用性
      • 消除了类与类之间的耦合:可以将客户端与现有类进行解耦,使其具有拓展性和可复用性
      • 提高可维护性:将实现细节封装到适配器中
    • 缺点
      • 增加代码的复杂性
      • 降低代码的性能:需要进行额外的转换和处理
js 复制代码
// 兼容Adaptee到target对象的逻辑中(将Adaptee类的接口转换成客户端所期望的另一种接口Target类接口),使得两个对象可以一起工作
// 目标接口
class Target {
  request() {
    return 'Target: 请求完成!';
  }
}

// 需要适配的对象
class Adaptee {
  specificRequest() {
    return 'Adaptee: 请求完成!';
  }
}

// 适配器对象
class Adapter extends Target {
  constructor(adaptee) {
    super();
    this.adaptee = adaptee;
  }

  request() {
    const result = this.adaptee.specificRequest();
    return `Adapter: ${result}`;
  }
}

// 使用适配器模式
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
console.log(adapter.request()); // 输出:Adapter: Adaptee: 请求完成!
axios中的适配器实践

axios更重要的一点是磨平了不同环境(node和浏览器)下的调用差异

在axios中的核心逻辑中,可以注意到实际上派发的是dispatchRequest方法,该方法内部主要做了两件事:

  • 数据转换,转换请求体/响应体。可以理解为数据层面的适配
  • 调用适配器
js 复制代码
// 若用户未手动配置适配器,则使用默认的适配器
var adapter = config.adapter || defaults.adapter;
  
  // dispatchRequest方法的末尾调用的是适配器方法
  return adapter(config).then(function onAdapterResolution(response) {
    // 请求成功的回调
    throwIfCancellationRequested(config);

    // 转换响应体
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    // 请求失败的回调
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // 转换响应体
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
js 复制代码
//磨平不同环境的逻辑 默认适配器
function getDefaultAdapter() {
  var adapter;
  // 判断当前是否是node环境
  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // 如果是node环境,调用node专属的http适配器
    adapter = require('./adapters/http');
    // http适配器具体逻辑
    // module.exports = function httpAdapter(config) {
      // return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
        // // 具体逻辑
      // }
    // }
  } else if (typeof XMLHttpRequest !== 'undefined') {
    // 如果是浏览器环境,调用基于xhr的适配器
    adapter = require('./adapters/xhr');
    // XHR中的适配器具体逻辑
    // module.exports = function xhrAdapter(config) {
      // return new Promise(function dispatchXhrRequest(resolve, reject) {
        // // 具体逻辑
      // }
    // }
  }
  return adapter;
}

不论采用哪种适配器,内部的逻辑都是两个适配器的入参都是config、两个适配器的出参都是Promise

JS中的装饰器

  • Property Decorator 属性装饰器

  • Class Decorator 类装饰器

    • 本质上装饰器是对一个类进行处理的函数,该函数是在编译时执行的函数,而非在运行时;装饰器的第一个参数就是所要装饰的目标类
    • 只要是Decorator后面是Class,默认就把Class当成参数隐式的传进Decorator里了
    ts 复制代码
    // 普通装饰器
    @decorator
    Class A{}
    // 等同于
    Class A{}
    A = decorator(A) || A
    
    // 带参数的类装饰器
    @decorator(true)
    class A{}
    
    //等同于
    fn = decorator(true)
    // 装饰器里再返回一个函数,通过返回的新函数来装饰类
    A = fn(A) || A
  • Methods Decorator 方法装饰器

  • Parameter Decorator 参数装饰器

ES7装饰器浅析

装饰器(Decorator)是一种新的语法特性,目的是让开发人员能够更加方便地向已有代码中添加新的功能或行为;

装饰器(Decorator)是一种函数,可以用来修改类中方法的行为,换句话说,装饰器是一种元编程技术,它允许开发人员在运行时修改代码的行为,而不用直接修改代码本身。装饰器可以用于类、类中方法、类中属性等。

装饰器是借鉴了其他强类型语言如Java、Python等中的装饰器逻辑,其内部是依赖于ES5中的Object.defineProperty方法

基础浅析
  • 装饰器的使用方式

    • 可以通过@符号类添加装饰器,在强类型语言中常用这种方式;
    • 如可以用@readonly装饰器来将类的属性设置为只读(都需要单独进行实现)
    js 复制代码
    // decorators.js
    function funcDecorator(target,name,decorator){
        // 缓存旧的方法 即下列的onClick方法
        let originalMethods = decorator.value
        // 修改对应的方法 添加新的逻辑如日志收集 (拦截并重写对应的装饰方法)
        decorator.value = function() {
            console.log('func的装饰器逻辑=========')
            // 返回旧的方法执行结果
            return originalMethods.apply(this,arguments)
        }
    
        // 返回装饰器
        return decorator
    }
    
    function readonly(target,name,decorator){
        decorator.writable = false
        return decorator
    }
    
    module.exports = {
        funcDecorator,
        readonly
    }
    js 复制代码
    import { funcDecorator,readonly } from "../decorators.js"
    class Button {
        @funcDecorator
        onClick() {
            console.log('func原有逻辑=========')
        }
        
        @readonly
        name() {return '姓名是:'+name }
    }
    
    const button = new Button()
    
    button.onClick()
    console.log(button.name('登录'),'button=========')
    button.name()
    console.log(button.name('登录'),'button=========')
    button.name = 123
    console.log(button.name,'button=========')
    
    // func的装饰器逻辑=========
    // func原有逻辑=========
    // 姓名是:登录 button=========
    // 姓名是:登录 button=========
    
    // 有**@readonly**
    // button.name = 123;
    // TypeError: Cannot assign to read only property 'name' of object '#<Button>'
    
    // 无**@readonly**
    // 123 button=========
    • 实现添加多个参数的功能(可以在装饰器外部再封装一层函数)
    js 复制代码
    function testable(name) {
        return function(target) {
          target.name = name;
        }
      }
    
    @testable('MyTestableClass')
    class MyTestableClass {}
    MyTestableClass.name // MyTestableClass
    
    @testable('MyClass')
    class MyClass {}
    MyClass.isTestable // MyClassf
    • 工作原理
      • 基础语法 - 三个参数解析
        • target:需要修改的类
        • name:需要修改的属性名
        • decorator:属性描述符

原理浅析

装饰器实际上是一个语法糖,正如class语法糖背后的原理实际上是ES5构造函数一样

  • @decorator的执行流程
    • 函数传参&调用
      • 主要是给类添加装饰器和给类中的函数添加装饰器,具体示例参考上面代码
        • 为类添加装饰器时,装饰器target就是该类
          • 装饰器中一般只需要一个target参数即可
        • 为类中的函数添加装饰器时,装饰器target就变成了该类的原型对象(Button.prototype),原因是类中的方法总是要依附于类的实例存在的,类内部的方法其实就是装饰了类的实例;
          • 装饰器需要至少三个参数,target目标装饰器对象、修饰的目标属性名、属性描述对象(具体逻辑参考ES5中的defineProperty)
          • 装饰器函数是在编译阶段就执行了,而类的实例是在运行时动态生成的额,因此不能为普通函数添加装饰器逻辑(存在变量提升问题)

具体实践

React中的装饰器:HOC(Higher Order Component:高阶组件)

高阶组件就是一个函数,且该函数接收一个组件作为参数,并返回一个新的组件
React中的高阶组件其实就是包装了另一个组件的React组件

  • 类装饰器中重写生命周期时的执行顺序问题(不会覆盖)
    • componentDidMount来说:
      • 先执行被装饰类的,然后再执行装饰器内的
    • componentWillUnmount来说
      • 先执行装饰器内的,然后再执行被装饰的类的
jsx 复制代码
// HocDecorators.jsx
import React, { Component } from 'react'

const borderHoc = WrappedComponent => class extends Component {
  render() {
    return <div style={{ border: 'solid 1px red' }}>
      <WrappedComponent />
    </div>
  }
}
module.exports = {
  borderHoc
}
js 复制代码
// HocComponent.jsx
import React, { Component } from 'react'
import borderHoc from '../HocDecorators.jsx'

// 用borderHoc装饰目标组件
@borderHoc 
class TargetComponent extends React.Component {
  render() {
    // 目标组件具体的业务逻辑
  }
}

// export出去的其实是一个被包裹后的组件
export default TargetComponent

高阶组件在实现层面来看就是前文提到的类装饰器

  • 多参数装饰器在React中的应用
js 复制代码
const setTitle = (title) => (wrappedComponent) => {
  // 白话文:一个函数里返回一个函数再返回一个组件包裹器
  // title => Page Login
  // wrappedComponent => PageLogin组件
  return class extends Component {
    componentDidMount() {
      document.title = title
    }

    render() {
      // 由于使用了wrappedComponent进行了包裹,因此需要通过{...this.props}来传递props的值到子组件中进行使用
      return <wrappedComponent {...this.props} />
    }
  }
}

@setTitle('Page Login')
class PageLogin extends Component{
  // ......
}

封装数据加载装饰器到React中

js 复制代码
import React, { Component } from 'react';
import http from 'utils/http';

export default function createWithPreload(config) {
//因为我们需要传一个url 参数,所以暴露一个 func
    return function withPreload(WrappedComponent) {
        return class extends Component {
           
           // 还是和以前的写法一样 在 ComponentDidMount 的时候取数据
            componentDidMount() {
                this.getData();
            }

            getData = async () => {
               
                try {
                    // config 作为唯一的传参
                    const data = await http(config);

                    this.setState({
                        data
                    });
                } catch (error) {
                    this.setState({
                        error
                    });
                }

            };

            render() {
                const { error, data } = this.state;

                if (error) {
                    return '数据错啦: ' + ${error}
                }

                // 返回的到的数据 loadDtat={data}
                // 因为用到了 WrappedComponent 包裹起来,所以需要使用 {...this.props} 去传递 props 的值给子组件
                return <WrappedComponent {...this.props} loadData={data} />;
            }
        };
    };
}


// 使用
import React, { Component } from 'react';
import withPreload from './withPreload';

// 虽然我们费了很多功夫完成了装饰器,但是现在我们只需要这样一句话就可以预加载我们需要的数据了,在很多页面都可以复用

@withPreload({
    url: getData('/xxx/xxx/test')
})

class index extends Component{
    render(){
        return <div>{this.props.loadData.data}</div>
    }
}

react 写一个预加载表单数据的装饰器

Redux中的装饰器模式

需要使用具体的依赖库进行兼容处理,不然不是原生支持装饰器的在Redux中,具体依赖于npm install babel-plugin-transform-decorators-legacy --save-dev,然后配置.babelrc配置文件的{"plugins":["transform-decorators-legacy"]}就可以实现Redux对装饰器的支持;
Redux是一个非常好的状态管理库;Redux的核心概念是Store、Action和Reducer,Store存储整个应用状态,Action表示用户对应的操作,Reducer是纯函数,根据Action更新Store的状态

js 复制代码
// 初识版本  强依赖于Redux API
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import userAction from "../action/Action";//action creator路径,换成自己的。
class MyReactComponent extends React.Component { }
function mapStateToProps(state){ 
    return state;
}
function mapDispatchToProps(dispatch){ 
    return bindActionCreators(userAction,dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);


// 简单的装饰器模式
@connect(mapStateToProps, mapDispatchToProps)
class MyReactComponent extends React.Component { }
function mapStateToProps(state){ 
  return state;
}
function mapDispatchToProps(dispatch){ 
  return bindActionCreators(userAction,dispatch)
}

// 简介版装饰器模式
@connect(state => state.user, dispatch => bindActionCreators(userAction, dispatch))
class MyReactComponent extends React.Component { }


// 单独封装提取后的装饰器
//connect.js
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import userAction from "../action/Action";
const mapStateToProps = state => state
const mapDispatchToProps = dispatch => bindActionCreators(userAction, dispatch)
export default connect(mapStateToProps, mapDispatchToProps)
//MyReactComponent.jsx
//组件上
import connect from './connect'
@connect
class MyReactComponent extends React.Component { }

//兼容较复杂的Store装饰器
/**
假设需要维护的Store数据是
{
main:{数据},
user:{数据},
page:{数据},
router:{数据},
}
*/

//baseConnectFactory.js
//函数工厂模块,可以接收数组
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
//stateKey是state里的key,action是传入的action creator
export default (stateKey,action)=>connect (
    state=>{
        let s = {}
        if(stateKey instanceof Array){
            stateKey.forEach(k=>s[k]=state[k])
        }else{
            s=state[stateKey]
        }
        return s
    },
    dispatch=>{
        let a = {}
        if(action instanceof Array){
            action.forEach(act=>a[act.name]=bindActionCreators(act,dispatch))
        }else{
            a = bindActionCreators(action,dispatch)
        }
        return a
    }
)
//MyReactComponentConnect.js
//在需要使用装饰器的地方传入真实数据,构造装饰器函数
import connect from './baseConnectFactory'//引入aseConnectFactory.js模块
import userAction from '../user/action/Action'//引入action creator模块
//导出装饰器函数
export default connect (['main','user'],userAction)
//MyReactComponent.jsx
//组件上
import connect from './MyReactComponentConnect'
//直接使用装饰器
@connect
class MyReactComponent extends React.Component { }

模块开发之react-redux使用装饰器函数Decorator

  • Redux中的两种装饰器模式
    • Reducer装饰器
      • 可以用于组织复杂的Reducer逻辑,使得代码更加简洁和易于维护
    • Middleware装饰器
      • 可以抽离出一些通用的Middleware逻辑,在应用中高效的复用相关逻辑
    • React理解装饰器👍🏻

其他应用场景

实际应用中,装饰器可以做的不止这些,可以抽象出某些页面共同的特点,使用装饰器创建公共的模版,其他被装饰的页面只要实现自己独特的部分即可

推荐文献

在Web应用中使用ES7装饰器

相关推荐
Json____15 分钟前
学法减分交管12123模拟练习小程序源码前端和后端和搭建教程
前端·后端·学习·小程序·uni-app·学法减分·驾考题库
上趣工作室28 分钟前
vue2在el-dialog打开的时候使该el-dialog中的某个输入框获得焦点方法总结
前端·javascript·vue.js
家里有只小肥猫28 分钟前
el-tree 父节点隐藏
前端·javascript·vue.js
fkalis29 分钟前
【海外SRC漏洞挖掘】谷歌语法发现XSS+Waf Bypass
前端·xss
zxg_神说要有光1 小时前
自由职业第二年,我忘记了为什么出发
前端·javascript·程序员
陈随易1 小时前
农村程序员-关于小孩教育的思考
前端·后端·程序员
云深时现月1 小时前
jenkins使用cli发行uni-app到h5
前端·uni-app·jenkins
昨天今天明天好多天2 小时前
【Node.js]
前端·node.js
2401_857610032 小时前
深入探索React合成事件(SyntheticEvent):跨浏览器的事件处理利器
前端·javascript·react.js
雾散声声慢2 小时前
前端开发中怎么把链接转为二维码并展示?
前端