TypeScript中类装饰器模式如何处理原类递归方法?
装饰器模式递归调用问题的解决方案
问题描述
使用经典装饰器模式(非TypeScript实验性装饰器特性)扩展Foo类的next方法时,遇到核心问题:Foo的next方法在特定条件下会递归调用自身,但递归时只会执行原方法逻辑,不会经过Bar等装饰器扩展的逻辑。
附原代码:
interface IFoo { next(): number; } class Foo implements IFoo { next(): number { console.log("Foo begin"); // ... if (this.abc()) return this.next(); // ... const result = this.xyz(); console.log("Foo end", result); return result; } protected abc() { return Math.random() < 0.5; } protected xyz(): number { return Math.random(); } } class FooDecorator implements IFoo { protected wrappee: IFoo; constructor(wrappee: IFoo) { this.wrappee = wrappee; } next() { return this.wrappee.next(); } } class Bar extends FooDecorator { next() { console.log("Bar begin"); const result = this.wrappee.next() + 1; console.log("Bar end", result); return result; } } let instance: IFoo = new Foo(); instance = new Bar(instance); instance.next();
核心原因
Foo类中this.next()的递归调用,其this指向的是原始Foo实例,而非外层被装饰器包裹后的完整实例。装饰器模式通过包裹原实例扩展功能,但原实例内部的this并不会自动指向装饰后的实例,导致递归时跳过装饰逻辑。
修复方案
方案1:修改Foo类,解耦递归调用
调整Foo的内部逻辑,让递归调用依赖一个可替换的引用,而非直接调用自身:
class Foo implements IFoo { // 持有当前完整实例的引用,默认指向自身 protected currentInstance: IFoo; constructor() { this.currentInstance = this; } next(): number { console.log("Foo begin"); // ... if (this.abc()) return this.currentInstance.next(); // 改用currentInstance调用 // ... const result = this.xyz(); console.log("Foo end", result); return result; } // 提供方法更新引用,让装饰器可以设置为装饰后的实例 setCurrentInstance(instance: IFoo) { this.currentInstance = instance; } protected abc() { return Math.random() < 0.5; } protected xyz(): number { return Math.random(); } }
实例创建时,需要将装饰后的实例回传给Foo:
const foo = new Foo(); const instance: IFoo = new Bar(foo); foo.setCurrentInstance(instance); instance.next();
这样递归时就会调用装饰后的next方法。
方案2:使用Proxy代理实现装饰
利用TypeScript的Proxy特性,直接拦截所有方法调用,包括内部递归的this.next():
// 定义Bar装饰器的Proxy实现 const createBarDecorator = (target: IFoo): IFoo => { return new Proxy(target, { get(target, prop) { if (prop === 'next') { return () => { console.log("Bar begin"); // 调用原方法,Proxy会拦截所有后续调用 const result = (target.next as () => number)() + 1; console.log("Bar end", result); return result; }; } // 其他属性直接返回原实例的对应值 return (target as any)[prop]; } }); }; // 使用方式 let instance: IFoo = new Foo(); instance = createBarDecorator(instance); instance.next();
这种方式无需修改Foo类的任何代码,Proxy会拦截所有对实例方法的访问,包括Foo内部递归调用的this.next(),因为this指向的是Proxy实例。
方案3:重构Foo的递归逻辑
如果允许修改Foo的核心逻辑,可以把递归逻辑抽离成独立方法,让装饰器可以重写该方法:
class Foo implements IFoo { next(): number { console.log("Foo begin"); // ... if (this.abc()) return this.recursiveNext(); // ... const result = this.xyz(); console.log("Foo end", result); return result; } // 抽离递归调用的方法,允许装饰器重写 protected recursiveNext(): number { return this.next(); } protected abc() { return Math.random() < 0.5; } protected xyz(): number { return Math.random(); } } // 修改Bar装饰器,继承Foo而非FooDecorator(仅当允许装饰器继承原类时适用) class Bar extends Foo { private wrappee: IFoo; constructor(wrappee: IFoo) { super(); this.wrappee = wrappee; } next() { console.log("Bar begin"); const result = this.wrappee.next() + 1; console.log("Bar end", result); return result; } // 重写递归方法,调用装饰后的next protected recursiveNext(): number { return this.next(); } }
这种方式需要调整装饰器的继承关系,仅适用于原类允许被继承扩展的场景。
是否需要更换设计模式?
不需要更换装饰器模式,它依然是扩展类功能的合适选择。上述方案都能解决递归调用的问题,其中Proxy方案最简洁且无需修改原类代码;如果不能使用Proxy,修改原类解耦递归调用的方案也能有效解决问题。
内容的提问来源于stack exchange,提问作者user64675
相关产品推荐
相关产品推荐

