代理模式与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,代码逻辑上划分的更清晰了。