本文已参与「新人创作礼」活动.一起开启掘金创作之路。
一、装饰者模式
以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。 能够在不改变对象自身,在程序运行期给对象动态加职责,与继承相比,装饰者是一种更灵活的做法。
二、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;
}