以下のようなサードパーティのクラスがあって、メソッドに渡された引数や結果をログに残すためにインターセプトしたい。
自分らが管理しているクラスなら、デコレーターを作るのがいいと思うが、サードパーティーのクラスを書き換えるのはちょっと嫌だ(保守性の低下や動作が壊れるリスク的な観点で)。
またクラス構造がネスト・継承されている場合、デコレーターが複雑になり保守もしにくくなりそうだなーと思っていた。
// thirdPartyClass.ts interface Data { [Key: string]: number; } export class ThirdPartyClass { private data: Data = {}; getData(key: string): number { return this.data[key]; } setData(key: string, value: number): void { this.data[key] = value; } reset(): void { this.data = {}; } }
Proxy を使う
JavaScript には Proxy という、オブジェクトのプロキシを作成するコンストラクタが標準である。関数呼び出しであれば、 apply
ハンドラーを使うことでインターセプトできる。
import { ThirdPartyClass } from "./thirdPartyClass"; function wrap<M extends string, I extends Record<M, Function>>(instance: I, methods: M[]) { const result = {} as Pick<I, M>; for (const key of methods) { result[key] = new Proxy<I[M]>(instance[key], { apply: (target: I[M], _: any, argArray: any[]) => { const res = target.apply(instance, argArray); console.log(`args: ${argArray}, res: ${res}`); return res; }, }); } return result; } const obj = new ThirdPartyClass(); const wrappedObj = wrap(obj, ["getData", "setData"]); wrappedObj.setData("my-key", 19); // args: my-key,19, res: undefined wrappedObj.getData("my-key"); // args: my-key, res: 19
Generics について
また、Generics が少し複雑なのでコメントを入れてみる。
この generics によって、定義されていないメソッド名を指定したり、関数以外のプロパティが指定されると型エラーになる。
こんな複雑な generics も書けるのか~と驚いた。
function wrap< M extends string, // 制約: M は string I extends Record<M, Function>, // 制約: I[M] は Function >( instance: I, methods: M[], // method=["add", "print"] の時 type M = "add" | "print" ) { /* Pick では渡された I の0個以上のプロパティ M の型を作る # 例 type I = { add: (a: number, b: number) => number; sub: (a: number, b: number) => number; print: (a: string) => string; }; type M = "add" | "print"; Pick<I, M> の型は以下になる { add: (a: number, b: number) => number; print: (a: string) => string; }; */ const result = {} as Pick<I, M>; }
参考
今回のような複雑な generics がいくつか説明されていました。