如何在 TypeScript 中每次调用静态类方法时调用静态方法?

3

我创建了一个单例(抽象)类,需要在大多数方法使用之前初始化一些数据。以下是该类的示例:

abstract class MyClass {
  private static initialize(): void {
    // do stuff
  }

  public static doStuff1(param1: string): string {
    MyClass.initialize();
    // do stuff
    return 'stuff1';
  }

  public static doStuff2(param2: string[]): string[] {
    MyClass.initialize();
    // do stuff
    return ['stuff2'];
  }
}

有没有一种方法可以避免在每个方法上添加 initialize() 调用?我考虑使用装饰器,并找到了这个相关的问题:Typescript | Call a function every time a function is called

不幸的是,它使用了传统的装饰器格式,至少这是我收到的错误信息:

Syntax error - Support for the experimental syntax 'decorators-legacy' isn't currently enabled

我正在尝试找到一个等效的格式使用第二阶段提案,但也许有更好的选择?


假设 initialize 设置了一些静态变量的值,这些变量被其他方法使用,你可以编写一个 get 方法来进行初始化。 - kaya3
@kaya3 我添加了一个简单的代码示例,但是 initialize 方法将填充多个静态属性,这些属性被多个方法使用 - 这就是为什么我正在寻找一种全局处理的方式。 - Nicolas Bouvrette
3个回答

2

(单例通常不被鼓励使用,但是由于我不知道您的用例,我会假设您有充分的理由使用它们,并且不会介绍其他替代方法)。

为了解决您的问题,您可以使用一个小包装工具来装饰您的函数:

const wrap = <T extends (...args: any[]) => any>(func: T): T =>
  <T>((...args: any[]) => {
    // Do something before every function here...
    return func(...args);
  });

你可以像这样定义函数:

abstract class MyClass {
  public static doStuff1 = wrap((param1: string): string => {
    // do stuff
    return 'stuff1';
  });
}

doStuff1函数现在保证始终在执行自己的代码之前执行wrap函数中的代码。

这种方法的问题在于,您无法访问MyClass的私有状态或函数,这可能是期望的。您可以每次包装它时传递一个方法引用,但那样您并不比在每个函数开头调用它更好。

更好的方法:非静态单例

如果您确实正在使用单例模式,那么以非静态方式实现它可能更好,并且可以免费获得保证一次初始化的好处:

class MyClass {
  private static instance?: MyClass;
  static getInstance = (): MyClass =>
    (MyClass.instance = MyClass.instance ?? new MyClass());

  private constructor() {
    // Do your initialization
    console.log("initializing");
  }

  doStuff1() {
    // do stuff
    console.log("doing stuff 1");
  }

  doStuff2() {
    // do stuff
    console.log("doing stuff 2");
  }
}

MyClass.getInstance().doStuff1();

// Output:
// initializing
// doing stuff 1

MyClass.getInstance().doStuff1();
MyClass.getInstance().doStuff2();

// Output:
// doing stuff 1
// doing stuff 2

请注意上述输出,初始化仅被调用一次,这似乎是所需的行为(根据您的问题)。


您能够详细说明为什么不鼓励使用单例吗?我能够想到很多只需要一个实例的对象(例如会话,路由等)。在这些情况下还有哪些其他选项可以使用? - Nicolas Bouvrette
关于单例模式的讨论可以在这个帖子中找到,解释了避免使用它们的原因。关于你的例子,只有一个实例的事实并不意味着你应该只允许一个:可能会出现需要多个实例同时存在的情况(例如在测试中,或者后来出现了一个未预料到的情况),每个实例都具有不同的状态。当使用单例模式时,你已经削减了这个选项。 - soimon
非常感谢您提供的详细信息-我喜欢您的回答并将接受它。 - Nicolas Bouvrette
这也会导致紧密耦合,你的代码到处都依赖于一个全局访问的单一代码片段。这使得你的代码难以重用和重构,因为你无法单独取出和测试/重用它们,而不必解开全局依赖的网络。如果您想了解更多关于这个特定的设计原则,请查找“依赖反转原则”和“依赖注入”。 - soimon

2

我同意soimons的答案,因为它鼓励编写更清晰的代码。不过,你可以使用代理对象来包装你的抽象类,这样你的调用就感觉像是在使用一个静态单例。

我不确定这是否会有副作用,但看起来它可以满足你的需求。

_MyClass.ts

abstract class _MyClass {
  private static count:number = 0;

  private static initialize(): void {
    console.log('initialized'+_MyClass.count++);
  }
  private static doStuff1(param1: string): string {
    return 'stuff1';
  }
  private static doStuff2(param2: string[]): string[] {
    return ['stuff2'];
  }
  public static handler2 = {
    get: function(target:any, prop:Function) {
      _MyClass.initialize();
      return Reflect.get(target,prop);
    }
  };
}
const MyClass = new Proxy(_MyClass, _MyClass.handler2);
export {MyClass};

index.ts

import { MyClass } from "./_MyClass"
alert(MyClass.doStuff1())

以上代码将生成以下内容:
[LOG]: "initialized0" 
[LOG]: "stuff1" 
[LOG]: "initialized1" 
[LOG]: ["stuff2"] 

1
不错,我之前还不了解 Proxy 对象。 - soimon

0
如果有人需要帮助,我最终采用了更简单的方法,基本上是导出非静态类的实例。因此,它不再是真正的单例,这意味着没有任何东西可以阻止初始化更多的实例,但是除了测试之外,实际上没有任何意义去这样做。
class MyClass {
  private prop1: string[];
  private prop2: string;

  constructor(): {
    // load data into the object
    this.prop1 = ['a', 'b'];
    this.prop2 = 'c';
  }

  public getProp1(param: string[]): string[] {
    return this.prop1;
  }

  public getProp2(param: string): string {
    return this.prop2;
  }

}

const myClass = new MyClass();
export default myClass;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接