3

I created a singleton (abstract) class that requires to have some data initialized before most methods can be used. This is an example of how the class looks like:

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'];
  }
}

Is there a way to avoid adding these initialize() call on each methods? I was thinking of using decorators and found this related question: Typescript | Call a function every time a function is called

Unfortunately, it is using the legacy decorator format, at least this is the error I am getting:

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

I was trying to find an equivalent format using the stage 2 proposal, but maybe there is a better option?

Nicolas Bouvrette
  • 4,295
  • 1
  • 39
  • 53
  • Presuming `initialize` sets the value of some static variable that is used by those other methods, you could write a `get` for that variable which does the initialization. – kaya3 Jan 03 '21 at 18:35
  • @kaya3 I added a simple code sample but the `initialize` method would populate several static properties that are used by several methods - this why I was looking at a way to do this globally – Nicolas Bouvrette Jan 03 '21 at 18:39

3 Answers3

2

(Singletons are generally discouraged, but as I don't know your use case I will assume you have good reasons to use them and won't go into alternatives).

In order to solve your problem, you could decorate your functions using a little wrapper utility:

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

You could then define your functions like this:

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

Function doStuff1 is now guaranteed to always execute the code in wrap before executing its own.

The problem with this approach is that you can't access the private state or functions of MyClass, which is likely desirable. You could pass a reference to the method every time you wrap it, but then you're not much better off than just calling it at the beginning of every function.

Better approach: non static singleton

If you are using a singleton anyhow, you might be better off implementing it in a non static fashion and have the benefits of guaranteed one time initialization for free:

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

Please note from the above output that initialization is called only once, which (judging by your question) seems to be the desired behavior.

soimon
  • 2,400
  • 1
  • 15
  • 16
  • can you expand on why Singletons are discouraged? I can think of many objects that would have a single instance (e.g. Sessions, Routes, etc.). What other options would you use for these cases? – Nicolas Bouvrette Jan 11 '21 at 17:51
  • A proper discussion about it can be found in [this thread](https://stackoverflow.com/questions/137975/what-is-so-bad-about-singletons), explaining the reasoning beside avoiding them. Regarding your examples, the fact that there is probably only one of those instances doesn't mean you should allow for only one: there may be situations (in testing for example, or an unforeseen case arises later) where you'd need multiple instances existing in parallel, each having a different state. When using a singleton you have cut off that option. – soimon Jan 11 '21 at 18:13
  • Great thanks for the details - I like your answer and will accept it. – Nicolas Bouvrette Jan 11 '21 at 18:16
  • It also leads to *tight coupling*, where your code all over the place relies on one single piece of code accessed globally. This makes your code hard to reuse and refactor, as you can't take single pieces out and test/reuse them individually without untangling the net of global dependencies. Look up the `dependency inversion principle` and `dependency injection` if you'd like to know more about this particular design principle. – soimon Jan 11 '21 at 18:18
2

I agree with soimons answer because it encourages a cleaner code. Nevertheless, you might use a proxy object to wrap your abstract class so that your calls feel like you were using a static singleton.

I am not sure if this has side effects, but it seems that it does what you need.

_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())

the above code will produce the following:

[LOG]: "initialized0" 
[LOG]: "stuff1" 
[LOG]: "initialized1" 
[LOG]: ["stuff2"] 
Bongo
  • 2,933
  • 5
  • 36
  • 67
0

If this helps anyone, I ended up going a much simpler route, basically exporting an instance of a non-static class. So it's no longer a true singleton which means nothing is preventing the initialization of more instances, but there is really no point (other than for tests) to do so.

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;
Nicolas Bouvrette
  • 4,295
  • 1
  • 39
  • 53