Z

代理模式与AOP编程(Proxy Pattern)

Provide a surrogate or placeholder for another object to control access to it.

代理模式非常好理解,只要提供一种机制,使它能代理其他对象的访问,就是代理模式。那么为什么非得要通过代理访问,而不直接访问源对象呢,这不是多此一举吗?

考虑以下场景,你的纯业务写在一个类或方法中,而你要对这个业务进行埋点、记录日志,难不成要把这些非业务代码加在纯业务逻辑里么?这显然是不合适的。

所以我们可以使用代理模式创建一个代理对象,用于代理业务逻辑本身,纯业务逻辑不发生变更,业务埋点、记录日志这些行为全都放在代理对象中进行,这样就合理多了。

 1abstract class DialogCls {
 2    show() {
 3    }
 4
 5    hide() {
 6    }
 7}
 8
 9class Dialog extends DialogCls {
10}
11
12class DialogProxy {
13    private proxyTarget: DialogCls;
14
15    constructor(target: DialogCls) {
16        this.proxyTarget = target;
17    }
18
19    hide(): void {
20        console.log("call hide");
21        this.proxyTarget.show();
22    }
23
24    show(): void {
25        console.log("call show");
26        this.proxyTarget.hide();
27    }
28}
29
30const dialogProxy = new DialogProxy(new Dialog());
31
32dialogProxy.show();
33dialogProxy.hide();

类DialogProxy实现了抽象类DialogCls的方法,当调用DialogCls实例的方法时,先进行日志记录等额外工作,然后再调用被代理对象的真实方法,这就是代理模式最简单的应用。

这种实现方式有些麻烦,每次我们都得新建两个实例才能真正工作,要是一不小心调用错了实例,那可能代理就不工作了,所以对上面的例子进行修改。

 1abstract class DialogCls {
 2    private readonly proxy: DialogProxy;
 3
 4    constructor() {
 5        this.proxy = new DialogProxy(this);
 6    }
 7
 8    getProxy() {
 9        return this.proxy;
10    }
11
12    show() {
13    }
14
15    hide() {
16    }
17}
18
19// ...
20
21const dialogProxy = new Dialog().getProxy();
22
23dialogProxy.show();
24dialogProxy.hide();

只需要在抽象类DialogCls中添加一个getProxy方法即可。

动态代理

上面的例子需要对每一个要代理的对象都创建一个代理类,也就是静态代理。假设我们对很多对象都要添加记录日志的功能,那就需要创建很多代理类来完成了,这显然有点麻烦,那么可不可以创建一个通用的代理类,用来代理所有对象呢?显然是可以的。

 1abstract class DialogCls3 {
 2    private readonly proxy: this;
 3
 4    constructor() {
 5        this.proxy = createProxy(this);
 6    }
 7
 8    show() {
 9    }
10
11    hide() {
12    }
13
14    getProxy() {
15        return this.proxy;
16    }
17}
18
19class Dialog3 extends DialogCls3 {
20
21}
22
23function createProxy<T extends Object>(target: T) {
24    return new Proxy(target, {
25        get(target: T, p: string | number | symbol, receiver: any): any {
26            console.log(`[log] ${ p.toString() } called`);
27            return Reflect.get(target, p, receiver);
28        },
29    });
30}
31
32
33const dialog3 = new Dialog3().getProxy();
34dialog3.hide();
35dialog3.show();

创建一个动态代理,可以使用JavaScript中的Proxy与Reflect来完成。现在任何方法都会经过Proxy及Reflect才能被调用,从此我们再也不用一个个手写静态代理了。有了动态代理,我们可以大批量的对象应用同一种行为。

AOP编程

AOP全称为“Aspect Oriented Program”,也就是“面向切面编程”。也许你会疑惑。切面?我写个代码还要动刀吗?其实这个切面是一个抽象概念,指某个对象层次结构中可替换的层,虽然还是优点难以理解,但举个例子就能一下子明白:中间件或者生命周期函数,都可以理解为切面。

假设我们有某个业务逻辑,它分成了几步,那么在这几步中间的地方就是切面,假设有beforeSendRequest, afterSendRequest, afterProcessData这三个函数可以由用户自定义实现,那么原本静态的业务逻辑加上动态的切面函数,就构成了AOP。

koa.js的洋葱模式,React的各种生命周期都可以理解为AOP,也就是在程序的声明周期或者流程中可以动态的加减流程。那么AOP和我们的代理模式有什么关系?

我们使用代理模式,一般是为原来的对象添加非业务逻辑上的功能,例如日志、埋点,或是将公共逻辑提取出来。那么现在就会有两种角色:

  • 被代理者(纯业务逻辑)
  • 各种代理(例如日志代理,埋点代理,公共逻辑代理)

但问题是现在代理本身和代理所承担的逻辑是混合在一起的,例如代理要将被代理对象放到新创建的线程中执行,这是代理本身的行为,而代理本身所承担的逻辑是要记日志的,那么他们放在一起就不太合适了。现在AOP就可以发挥作用了。

我们把代理本地两个逻辑之间的地带作为切入点,将记日志逻辑传递进代理,这样就组合成了新创建线程执行并记日志的代理。

现在整个代理模型变成了三个角色:

  • 被代理者(纯业务逻辑)
  • 各种代理逻辑(例如日志代理,埋点代理,公共逻辑代理)
  • 代理本地(例如直接执行,新建线程执行,新建进程执行)

使用了AOP,代码逻辑上划分的更清晰了。