美文网首页
装饰器(Decorator)

装饰器(Decorator)

作者: 世玮 | 来源:发表于2021-04-30 08:26 被阅读0次

    装饰器(Decorator)

    装饰器是一种函数,写成@ + 函数名。它可以放在类和类方法的定义前面。

    类上的装饰

    @decorator
    class A {}
    
    // 等同于
    
    class A {}
    A = decorator(A) || A;
    
    • 装饰器是一个对类进行处理的函数。装饰器函数的第一个参数,就是所要装饰的目标类。
    • 装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数。

    React 与 Redux 库结合使用时,运用装饰器,更容易理解:

    class MyReactComponent extends React.Component {}
    
    export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
    

    修改成:

    @connect(mapStateToProps, mapDispatchToProps)
    export default class MyReactComponent extends React.Component {}
    

    方法上的装饰

    function readonly(target, name, descriptor){
        console.log(target, name, descriptor);
        // descriptor对象原来的值如下
        // {
        //   value: specifiedFunction,
        //   enumerable: false,
        //   configurable: true,
        //   writable: true
        // };
        // descriptor.writable = false;
        return descriptor;
    }
    export default class Girl{
        constructor(props) {
            this.weight = "6.5斤";
            this.age = 1;
        }
        @readonly
        name(){
            return `${this.weight} ${this.age}`
        }
    }
    
    image
    • 装饰器函数一共可以接受三个参数:
    function readonly(target, name, descriptor){
      // descriptor对象原来的值如下
      // {
      //   value: specifiedFunction,
      //   enumerable: false,
      //   configurable: true,
      //   writable: true
      // };
      descriptor.writable = false;
      return descriptor;
    }
    
    readonly(Girl.prototype, 'name', descriptor);
    // 类似于
    Object.defineProperty(Girl.prototype, 'name', descriptor);
    
    • 1、装饰器第一个参数是类的原型对象;上例是Girl.prototype
    • 2、第二个参数是所要装饰的属性名;
    • 3、第三个参数是该属性的描述对象;

    在实现一个日志输出场景:

    function log(target, name, descriptor) {
        var oldValue = descriptor.value;
    
        descriptor.value = function() {
            console.log(`Calling ${name} with`, arguments);
            return oldValue.apply(this, arguments);
        };
    
        return descriptor;
    }
    
    export default class Girl{
        constructor(props) {
            this.weight = "6.5斤";
            this.age = 1;
        }
    
        @log
        fetchAge(newAge){
            this.age = newAge;
        }
    }
    
      var girl = new Girl();
      girl.fetchAge(3); //这边可以看到日志打印
      console.log(girl.age);// 3
    

    一个方法多个装饰的场景:

    如果同一个方法有多个装饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行

    function logNew(methodName) {
        console.log(methodName);
        return (target, name, descriptor)=>{
            console.log('evaluated-methodName', methodName);
            var oldValue = descriptor.value;
            descriptor.value = function() {
                console.log(`Calling ${methodName} with`, arguments);
                return oldValue.apply(this, arguments);
            };
            return descriptor;
        }
    }
    
    export default class Girl{
        constructor(props) {
            this.weight = "6.5斤";
            this.age = 1;
        }
        @logNew("fetchAge1")
        @logNew("fetchAge2")
        fetchAge(newAge){
            this.age = newAge;
        }
    }
    
    var girl = new Girl();
    girl.fetchAge(3);
    
    //fetchAge1
    //fetchAge2
    //evaluated-methodName fetchAge2
    //evaluated-methodName fetchAge1
    //Calling fetchAge1 with
    //Calling fetchAge2 with
    

    使用装饰器实现自动发布事件

    import postal from 'postal';
    
    function publish(topic, channel) {
        const channelName = channel || '/';
        const msgChannel = postal.channel(channelName);
        msgChannel.subscribe(topic, v => {
            console.log('频道: ', channelName);
            console.log('事件: ', topic);
            console.log('数据: ', v);
        });
    
        return function(target, name, descriptor) {
            const oldValue = descriptor.value;
    
            descriptor.value = function() {
                let value = oldValue.apply(this, arguments);
                msgChannel.publish(topic, value);
            };
        };
    }
    
    
    export default class Girl{
        constructor(props) {
            this.weight = "6.5斤";
            this.age = 1;
        }
    
        @publish('Girl.fetchWight', 'fetchWight')
        fetchWight(newWeight) {
            this.weight = newWeight;
    
            return this;
        }
    }
    
    var girl = new Girl();
    girl.fetchWight('8.5斤');
    

    其他使用场景:

    • 1、core-decorators是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器。
    • 2、Mixin ; “混入”
    • 3、Trait; traits-decorator也是一种装饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等;Trait 不允许“混入”同名方法
    @traits(TFoo, TBar::excludes('foo'))
    class MyClass { }
    
    @traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
    class MyClass {}
    
    @traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}}))
    class MyClass {}
    

    react埋点插件整理

    1. react-tag-component

    2. trackpoint-tools

    常见的埋点事件:

    背景:刚接触这个需求的时候,如果面向过程的实现的时候,我们常常会把业务逻辑和埋点行为混为一谈;可能也尝试做了一些的变量或者函数的抽离,但是久而久之随着项目的拓展,相应的功能还是很难维护和可读。

    场景1:页面加载埋点和格式化

    //原始代码:
    componentDidMount() {
        this.initPage();
        //...doSomething
        //然后进行页面初始化埋点:
       const properties={
          "page_name":"充值结果页",
          "btype": "对应的按钮类型",
          "project_type":"对应的项目类型",
        };
        pageViewEvent('result',properties)
    }
    
    //优化代码:
    @boundPageView("testInit", "h5入口page", "testInit")
    componentDidMount() {
        this.initPage();
        //...doSomething
    }
    //然后我们只要去思考怎么实现boundPageView;并增加了可读性。
    

    场景2:按钮行为埋点

    //原始代码; 比如某个banner点击
    bannerClick = () => {
      //do banner click ...
      const eventInfo={
        "page_name":"结果页",
        "btn_name":"跳转结果页",
        "target_url":"",
        "btype": "对应的按钮类型",
        "project_type":"对象的项目类型",
      }
      trackEvent("banner_click",eventInfo)
      
      //todo somethings
    };
    //原始代码; 比如下单结果提交
    payCommit = ()=>{
          //todo: 各种下单 操作
          const eventInfo={
            "sale_price":0,
            "creatorder_time": '2021-04-19 00: 00:00',
            "btype": "项目类型",
            //...其他好多参数
          }
          trackEvent("charge",eventInfo)
          //todo: 各种下单操作 
    }
    

    缺点:

    • 1、业务逻辑跟埋点事件混为一谈;
    • 2、可读性,可拓展性差;
    • 3、事件的主次参数等不明确;
    • 4、代码冗余严重
      。。。
    //优化代码:
    //针对简单的通用按钮点击事件埋点:
    //直接约定固定的必要的参数
    @trackBtnClick("btn_click", "testInit", '点击按钮', "testInit")
    testBtnEvent = ()=>{
        console.log("testBtnEvent start");
    }
    
    
    //针对自定义参数的通用按钮点击事件埋点:
    @trackBtnClickWithParam("btn_click", {
        "btn_name": "testInit",
        "btype": "点击按钮",
        "project_type": "testInit",
    })
    testBtnEventParams = ()=>{
        console.log("testBtnEventParams start");
    }
    
    //针对传值很多,且需要进行一些处理的参数;
    //当前场景可以可以拆分主要实现,在装修器中传入通用的必要参数,
    //其他细节参数,直接从转换好的参数中获取。
    testPayEvent = (params)=>{
        console.log("testPayEvent start===>", JSON.stringify(params));
    
        this.payEventProcess({
            "sale_price": '',
            "original_price": '',
            "is_discontcoupon": false,
            "discountcoupon_id":'',
            "discountcoupon_price":0,
            "discountcoupon_name":'',
            "buy_num": 1,
            "charge_connect":"商品名称",
            "account": "充值账号",
            "creatorder_time":'2021-04-19 00:00:00',
        });
    }
    
    @trackBtnClickWithArgs("charge", {
        "project_type": "testInit",
        "btype": "test_recharge"
    })
    payEventProcess = (args)=>{
        console.log("testPayEvent end");
    }
    

    具体装饰器完整代码实现:

    import curryN from 'lodash/fp/curryN'
    import propSet from 'lodash/fp/set'
    import isFunction from 'lodash/fp/isFunction'
    // ...省略部分 埋点事件
    
    /**
     * 绑定 页面初始化埋点
     * @param projectType
     * @param pageName
     * @param btype
     * @returns {function(*, *, *)}
     */
    export function boundPageView(projectType, pageName, btype) {
        return (target, name, descriptor)=>{
            var fn = descriptor.value;
            descriptor.value = function() {
                console.log(projectType, pageName, btype);
                try{
                    registerSuperProperty('project_type', projectType);
                    const properties = {
                        "page_name": pageName,
                        "btype": btype,
                        "project_type": projectType,
                    }
                    pageViewEvent(pageName, properties);
                }catch (e) {
                    console.log(e);
                }
                return fn.apply(this, arguments);
            };
            return descriptor;
        }
    }
    
    /**
     * 绑定按钮点击埋点
     * @param eventName
     * @param eventInfo
     * @returns {function(*, *, *)}
     *
     * @trackBtnClick(projectType, '用户访问首页', btype)
     * @track(before(() => {boundBtnClick(projectType, '用户访问首页', btype)}))
     */
    export function boundBtnClick(eventName, eventInfo) {
        console.log(eventName,"=====》", eventInfo);
        trackEvent(eventName || "btn_click", eventInfo)
    }
    
    //普通按钮事件埋点
    export function trackBtnClick(eventName, projectType, btnName, btype) {
        let partical = before((args)=>{
            const eventInfo={
                "btn_name": btnName,
                "btype": btype,
                "project_type":projectType,
            }
            boundBtnClick(eventName, eventInfo)
        })
        return track(partical)
    }
    
    //带自定义参数的按钮事件埋点
    export function trackBtnClickWithParam(eventName, params) {
        let partical = before((args)=>{
            boundBtnClick(eventName, params)
        })
        return track(partical)
    }
    
    //带部分参数&&取参数作为自定义参数的按钮事件埋点
    export function trackBtnClickWithArgs(eventName, params) {
        let partical = before((args)=>{
            boundBtnClick(eventName, {...args, ...params})
        })
        return track(partical)
    }
    
    //柯里化定义 埋点函数
    export const before = curryN(2, (trackFn, fn) => function (...args) {
        // console.log(trackFn, fn);
        try {
            isFunction(trackFn) && trackFn.apply(this, args)
        } catch(e) {
            console.error(e)
        }
    
        return fn.apply(this, args)
    })
    
    //track 装饰器 ;执行相应的柯里化函数
    export const track = partical => (target, key, descriptor) => {
        if (!isFunction (partical)) {
            throw new Error('trackFn is not a function ' + partical)
        }
        const value = function (...args) {
            return partical.call(this, descriptor.value, this).apply(this, args)
        }
        if (descriptor.initializer) {
            return propSet('initializer', function() {
                const value = descriptor.initializer.apply(this);
                return function (...args) {
                    return partical.call(this, value, this).apply(this, args);
                }
            }, descriptor);
        }
        return propSet('value', value, descriptor)
    }
    

    以上,主要通过装饰器,实现在方法处理之前进行了一些优化;这边只是一个思路,像常见的事件防抖,日志的记录,在同步函数或者异步函数之后,定时任务,计算函数的执行时间等通用功能,都可以用装饰器巧妙的实现。

    Spring项目中自定义注解的使用

    场景: 自定义了一些api;但是想每个方法前实现一个开关控制;

    • step1: 我们会很容易想到,定义一个通用的preCheck函数,然后每个api执行时,先预校验下;
    • step2: 自然就会想到如何用自定义注解完善呢?

    1.创建自定义注解类:

    import java.lang.annotation.*;
    
    @Documented
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface EquityApiLock {
    
        String value() default "";
    }
    

    这里注解类上的三个注解称为元注解,其分别代表的含义如下:

    • @Documented:注解信息会被添加到Java文档中
    • @Retention:注解的生命周期,表示注解会被保留到什么阶段,可以选择编译阶段、类加载阶段,或运行阶段
    • @Target:注解作用的位置,ElementType.METHOD表示该注解仅能作用于方法上

    2.创建面向切面类:

    @Component
    @Aspect
    @Slf4j
    public class EquityApiAspect {
    
    
        @Pointcut("@annotation(PKG.EquityApiLock)")
        private void pointcut() {}
    
        @Before("pointcut() && @annotation(equityApiLock)")
        public void advice(EquityApiLock equityApiLock) {
            log.info("EquityApiLock check==>{}", equityApiLock.value());
            String equityLock = ConfigService.getProperty("equity.apiLock", "1");
            if(StringUtils.equals("1", equityLock)){
                throw new BusinessException(CommonExceptionConstants.PARAM_INVALID);
            }
        }
    
    }
    

    3、使用:

        @EquityApiLock(value="getAllowances")
        @GetMapping("/getAllowances")
        public ResultInfo<String> getAllowances(HttpServletRequest httpServletRequest) {
           //todo: do somethings
        }
    

    大功告成~

    通过这个方式,还可以完善接口操作日志收集或者流控等场景。

    相关文章

      网友评论

          本文标题:装饰器(Decorator)

          本文链接:https://www.haomeiwen.com/subject/apjurltx.html