ts装饰者模式与AOP【完美版】

602 阅读4分钟

本文已参与「新人创作礼」活动.一起开启掘金创作之路。

一、装饰者模式

以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。 能够在不改变对象自身,在程序运行期给对象动态加职责,与继承相比,装饰者是一种更灵活的做法。

二、AOP

面向切面编程,是对 OOP 的补充。利用AOP可以对业务逻辑的各个部分进行隔离,也可以隔离业务无关的功能比如日志上报、异常处理等,从而使得业务逻辑各部分之间的耦合度降低,提高业务无关的功能的复用性,也就提高了开发的效率。 在 Js 中,我们可以通过装饰者模式来实现 AOP,但是两者并不是一个维度的概念。 AOP 是一种编程范式,而装饰者是一种设计模式。

三、装饰者的实现方式

1. ES3 下装饰者的实现

// 原函数
var takePhoto =function(){
  console.log('拍照');
}
// 定义 aop 函数
var after=function( fn, afterfn ){ 
  return function(){
    let res = fn.apply( this, arguments ); 
    afterfn.apply( this, arguments );
    return res;
  }
}
// 装饰函数
var addFilter=function(){
  console.log('滤镜');
}
// 用装饰函数装饰原函数
takePhoto=after(takePhoto,addFilter);
takePhoto();

这样就实现了抽离拍照与滤镜逻辑,如果以后要自动上传,也可通过aop函数after来添加。

2. ES5 下装饰者的实现

在 ES5 中引入了Object.defineProperty,可以更方便的给对象添加属性:

let takePhoto = function () {
    console.log('拍照片');
}
// 给 takePhoto 添加属性 after
Object.defineProperty(takePhoto, 'after', {
    writable: true,
    value: function () {
        console.log('加滤镜');
    },
});
// 给 takePhoto 添加属性 before
Object.defineProperty(takePhoto, 'before', {
    writable: true,
    value: function () {
        console.log('打开相机');
    },
});
// 包装方法
let aop = function (fn) {
    return function () {
        fn.before()
        fn()
        fn.after()
    }
}
takePhoto = aop(takePhoto)
takePhoto()

3. 基于原型链和类的装饰者实现

我们知道,在 JavaScript 中,函数也好,类也好都有着自己的原型,通过原型链我们也能够很方便的动态扩展,以下是基于原型链的写法:

class Test {
	takePhoto() {
		console.log('拍照');
  }
}
// after AOP
function after(target, action, fn) {
	let old = target.prototype[action];
  if (old) {
    target.prototype[action] = function () {
      let self = this;
      fn.bind(self);
      fn(handle);
    }
  }
}
// 用 AOP 函数修饰原函数
after(Test, 'takePhoto', () => {
  console.log('添加滤镜');
});
let t = new Test();
t.takePhoto();

4. 使用 ES7 修饰器实现装饰者

在 ES7 中引入了@decorator 修饰器。目前Babel转码器已经支持。注意修饰器只能装饰类或者类属性、方法。三者的具体区别请参考 MDN Object.defineProperty

function after(target, key, desc) {
  const { value } = desc;
  desc.value = function (...args) {
    let res = value.apply(this, args);
    console.log('加滤镜')
    return res;
  }
  return desc;
}
class Test{
  @after
  takePhoto(){
    console.log('拍照')
  }
}
let t = new Test()
t.takePhoto()

四、经典项目完美实战【thinkts项目】

例如在koa中,可以模仿nest.js的装饰器来实现参数装饰器,类装饰器,方法装饰器,属性装饰器。那么就可以实现ts版本的spring生态,而且可以天马行空,高度定制化。

import {Conf} from "../config";import * as _ from "koa-compose";import * as Router from "koa-router";
const ROUTER=new Router(),Server={};let Routes=[],$b=true,$once=true,$=null,$Override=[];
/**参数装饰器,一般用于形参[B,P,Q,R,S,T]*/
const B:Function=(t,k,i:number)=>{t[k][i]="$"}//ctx.request.body
const P:Function=(t,k,i:number)=>{t[k][i]="params"}//ctx.params
const Q:Function=(t,k,i:number)=>{t[k][i]="query"}//ctx.query
const R:Function=(t,k,i:number)=>{t[k][i]="response"}//ctx.response
const S:Function=(t,k,i:number)=>{t[k][i]="querystring"}//ctx.querystring
const T:Function=(t,k,i:number)=>{t[k][i]="request"}//ctx.request
/**方法装饰器,一般用于路由的路径配置[app.get,app.post,app.put,app.del]*/
const app = {
  get:(r="")=>(t,k,d)=>{Routes.push({a:k,m:"get",r:r.charAt(0)==="/"?r:r===""?r:"/"+r});param(d.value,d)},
  post:(r="")=>(t,k,d)=>{Routes.push({a:k,m:"post",r:r.charAt(0)==="/"?r:r===""?r:"/"+r});param(d.value,d)},
  put:(r="")=>(t,k,d)=>{Routes.push({a:k,m:"put",r:r.charAt(0)==="/"?r:r===""?r:"/"+r});param(d.value,d)},
  del:(r="")=>(t,k,d)=>{Routes.push({a:k,m:"delete",r:r.charAt(0)==="/"?r:r===""?r:"/"+r});param(d.value,d)}
}
export {ROUTER,B,P,Q,R,S,T,app,_,Class,Id,Get,Post,Put,Del,Middle,Inject};
/** 类装饰器  用于配置 @param v path路径,或者是t  @param t curd等方法的Array<string>*/
let Class=(v:string|Array<"add"|"del"|"fix"|"info"|"page">="",t?:Array<"add"|"del"|"fix"|"info"|"page">)=>_=>{
  if(v==="")v=null;else if(typeof v!=="string"){t=v;v=null;}let $a:Array<{r:string,m,a,f:number}>=[];
  const V=_.name.replace(/([A-Z][a-z]+)[A-Z_]?\w*/,()=>(_.name==="View"?"":RegExp.$1));v=v??V.toLowerCase();
  if(typeof v==="string"){if(v.charAt(0)!=="/")v="/"+v;if(v==="/"){v="";}}
  v!==""&&Object.getOwnPropertyNames(_.prototype).forEach(k=>{
    if(typeof _.prototype[k]==="function")$Override.push(k);
  });
  if(t!==undefined)t.filter(p=>!$Override.includes(p)).forEach(p=>{
    switch (p) {
      case "add":Routes.push({a:p,m:"post",r:""});break;
      case "del":Routes.push({a:p,m:"delete",r:"/:id"+(_["##"]||"(\\d+)")});break;
      case "fix":Routes.push({a:p,m:"put",r:"/:id"+(_["##"]||"(\\d+)")});break;
      case "info":Routes.push({a:p,m:"get",r:"/:id"+(_["##"]||"(\\d+)")});break;
      case "page":Routes.push({a:p,m:"get",r:""});break;
      default:throw new Error("Wrong entry!");
    }
  });
  for (let r=0,l=Routes.length;r<l;++r){
    Routes[r].r=v+Routes[r].r;if(Conf.printRoute)$a.push(Routes[r]);let A
    const M=Routes[r].m,W=Routes[r].w?[Routes[r].r,Routes[r].w]:[Routes[r].r]
    if($Override.indexOf(Routes[r].a)>-1)A=_.prototype[Routes[r].a].bind($);else 
    A=_.prototype[Routes[r].a].bind(v===""?$:$[_.prototype["#"]]);
    ROUTER[M](...W,async(ctx,next)=>{ctx.body=await A(ctx,next)});
  }_=$=null;$Override.length=Routes.length=0;
}/** 类装饰器 @param v 配置允许的主键格式  @param t 匹配后的是:字符串、还是数字(默认字符串)*/
let Id=(v,t:"string"|"number"="string")=>_=>{if($===null)throw new Error("@Class needs to be implemented later.");_["##"]=v;_["_#"]=t;}
/**方法装饰器 一般用作路由,其实跟上面的app是一样的,区别是上面只需要引入一个app而不是下面四个*/
let Get=(r="")=>(t,k,d)=>{Routes.push({a:k,m:"get",r:r.charAt(0)==="/"?r:r===""?r:"/"+r});param(d.value,d)}
let Post=(r="")=>(t,k,d)=>{Routes.push({a:k,m:"post",r:r.charAt(0)==="/"?r:r===""?r:"/"+r});param(d.value,d)}
let Put=(r="")=>(t,k,d)=>{Routes.push({a:k,m:"put",r:r.charAt(0)==="/"?r:r===""?r:"/"+r});param(d.value,d)}
let Del=(r="")=>(t,k,d)=>{Routes.push({a:k,m:"delete",r:r.charAt(0)==="/"?r:r===""?r:"/"+r});param(d.value,d)}
let Middle=(...r:Array<_.Middleware<any>>)=>(t,k)=>{
  let f=Routes[Routes.length-1];if(f.a!==k){ throw new Error(t.constructor.name+":"+k+" use @Middle has to be on the top!");
  }else if(f.w){throw new Error("@Middle can only be used once!")}else{f.w=r.length===1?r[0]:_(r)}f=null
}
/**属性装饰器,一般是用于注入服务类,可以实例化服务类,无重复的实例化从而节省时间*/
let Inject=v=>(t,k)=>{
  if(t.constructor.name.replace(/([A-Z][a-z]+)[A-Z_]\w*/,"$1")===v.name.replace(/(\w*)[A-Z$]\w*/,"$1")){t["#"]=k;}
  if(Server[v.name]===undefined){Server[v.name]=new(v)}if($===null)$={};Object.defineProperty($,k,{enumerable:true,value:Server[v.name]})
}
let param=(m:Function,d)=>{
  let o=Object.keys(m),num=-1;for (let p in m){
    switch (m[p]) {
      case "$":num=Number(p);break;
      case "query":Routes[Routes.length-1].f=1;break;
      case "querystring":Routes[Routes.length-1].f=2;break;
      default:break;
    }
  };
  if(o.length){
    switch (o.length) {
      case 1:d.value=num===-1?async function(ctx,next:Function){
          return await m.call(this,ctx[m[0]],next);
        }:async function(ctx,next:Function){
          return await m.call(this,ctx.request.body,next);
        };break
      case 2:d.value=num===-1?async function(ctx,next:Function){
          return await m.call(this,ctx[m[0]],ctx[m[1]],next);
        }:num===0?async function(ctx,next:Function){
          return await m.call(this,ctx.request.body,ctx[m[1]],next);
        }:async function(ctx,next:Function){
          return await m.call(this,ctx[m[0]],ctx.request.body,next);
        };break
      case 3:d.value=num===-1?async function(ctx,next:Function){
          return await m.call(this,ctx[m[0]],ctx[m[1]],ctx[m[2]],next);
        }:num===0?async function(ctx,next:Function){
          return await m.call(this,ctx.request.body,ctx[m[1]],ctx[m[2]],next);
        }:num===1?async function(ctx,next:Function){
          return await m.call(this,ctx[m[0]],ctx.request.body,ctx[m[2]],next);
        }:async function(ctx,next:Function){
          return await m.call(this,ctx[m[0]],ctx[m[1]],ctx.request.body,next);
        };break
      case 4:d.value=num===-1?async function(ctx,next:Function){
        return await m.call(this,ctx[m[0]],ctx[m[1]],ctx[m[2]],ctx[m[3]],next);
        }:num===0?async function(ctx,next:Function){
          return await m.call(this,ctx.request.body,ctx[m[1]],ctx[m[2]],ctx[m[3]],next);
        }:num===1?async function(ctx,next:Function){
          return await m.call(this,ctx[m[0]],ctx.request.body,ctx[m[2]],ctx[m[3]],next);
        }:num===2?async function(ctx,next:Function){
          return await m.call(this,ctx[m[0]],ctx[m[1]],ctx.request.body,ctx[m[3]],next);
        }:async function(ctx,next:Function){
          return await m.call(this,ctx[m[0]],ctx[m[1]],ctx[m[2]],ctx.request.body,next);
        };break
      default:throw new Error("Wrong parameter!");
    }
  }o=d=null;
}