Skip to content

Commit

Permalink
Singletons in JavaScript
Browse files Browse the repository at this point in the history
  • Loading branch information
arendjr committed Nov 9, 2024
1 parent 812c53f commit 643092d
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,6 @@ post-architecture. Some more technical guidance so to say.
I have taken my time explaining the abstract, so that hopefully the technical
advice can be placed in the right context. But more technical advice _is_
coming... in the next post in this series. Stay tuned!

**Update:** The next post in this series is here:
[Post-Architecture: Premature Abstraction Is the Root of All Evil](/blog/2024/07/post-architecture-premature-abstraction-is-the-root-of-all-evil/).
3 changes: 2 additions & 1 deletion phebe/src/content/blog/post-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,6 @@ approach deserves a word on its own.

This is what I will call Post-Architecture.

**Update:** I've also written a follow-up, in case you want to know more about
**Update:** I've also written two follow-ups, in case you want to know more about
[what Post-Architecture is and isn't](/blog/2024/07/post-architecture-what-it-is-and-isnt).
Or for a more techncial perspective, jump straight to [Post-Architecture: Premature Abstraction Is the Root of All Evil](/blog/2024/07/post-architecture-premature-abstraction-is-the-root-of-all-evil/).
159 changes: 159 additions & 0 deletions phebe/src/content/blog/singletons-in-javascript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
title: "Singletons in JavaScript"
description: "Various ways of using singletons in JavaScript, with their pros and cons"
pubDate: "Nov 9 2024"
---

## Introduction

The [singleton pattern](https://en.wikipedia.org/wiki/Singleton_pattern) is a bit of a controversial
pattern, sometimes even considered an anti-pattern.

There are good uses for it (mostly convenience), but before using them you should consider these
downsides:

* Singletons can make it harder to write unit tests for code that uses them.
* If you have multiple singletons that call into one another, you can easily find yourself
entangling their concerns. If you find it becomes hard to track what happens where, you should
probably reconsider your approach.

So be mindful when using them, but as long as you avoid the pitfalls, a singleton can sometimes be
the simplest tool for the job. In this post, I'll present four different ways we can use one in
JavaScript.

## Using module state

```js
let privateState;

export function publicFunction() {
// Do stuff with `privateState`.
}
```

Wait, what's that? That doesn't look like a singleton at all! Indeed there are no classes, so there
also isn't a singleton instance of one. This example simply relies on the realization that
JavaScript has proper modules, and we can use module state to the same effect as state inside a
singleton instance.

This super simple example has all the same benefits as a singleton: Namely, convenient access to
functions that can modify private state. And naturally, it shares the same downsides as well.

## Using a class and an instance function

```js
let instance;

export function getMyInstance() {
if (!instance) {
instance = new MyClass();
}
return instance;
}

class MyClass {
#privateState;

publicMethod() {
// Do stuff with `#privateState`.
}
}
```

Okay, now we have something that looks more like a traditional singleton. It adds a bunch of
ceremony compared to the previous example, and you should carefully consider whether that ceremony
is worth it: You now have a class instance, but what's the purpose of that instance when the idea
behind a singleton is that there can only be a single instance, which you _shouldn't_ pass around?

The main benefit of this approach is that it may make lazy initialization a little easier.

One thing to note: Because we don't export `MyClass`, we prevent consumers of the singleton from
creating their own instances, which is exactly what we want.

## Using a class with a static instance method

The previous example could also be rewritten using a static instance method:

```js
export class MyClass {
static #instance;

#privateState;

static getInstance() {
if (!MyClass.#instance) {
MyClass.#instance = new MyClass();
}
return MyClass.#instance;
}

publicMethod() {
// Do stuff with `#privateState`.
}
}
```

Personally I find `static` to be mostly a redundant concept in JavaScript, since as the previous
example shows, we already have module-level variables that can do the same thing. But I understand
some people like them for familiarity reasons, or because they feel it connects the variable to
the class more.

Unfortunately, because the class is now exported, code outside the module can now use
`new MyClass()` to create instances, which is what we don't want.

If you use TypeScript, that disadvantage is easily remedied by adding a `private constructor() {}`,
but if you use plain JS you can't. At the same time, if you use plain JS, you don't have static
typing either, so I guess it's par for the course.

## Using a class with a static instance method and a guarded constructor

But if you really insist on using plain JavaScript and also on using static methods _and_ guarding
the constructor, you can do so like this:

```js
export class MyClass {
static #instance;
static #instantiating = false;

#privateState;

static getInstance() {
if (!MyClass.#instance) {
MyClass.#instantiating = true;
MyClass.#instance = new MyClass();
MyClass.#instantiating = false;
}
return MyClass.#instance;
}

constructor() {
if (!MyClass.#instantiating) {
throw new Error("Use MyClass.getInstance() instead");
}
}

publicMethod() {
// Do stuff with `#privateState`.
}
}
```

## Recommendations

As we can see, there's plenty of flavors to choose from.

My personal recommendation would be to stick with the first or second example, depending on your
needs. If you use TypeScript, you may opt to use the third, but it's mostly a matter of taste.

As I explained in my post on
[Post-Architecture](/blog/2024/07/post-architecture-premature-abstraction-is-the-root-of-all-evil/),
I have a strong preference towards simplicity. So if you feel tempted by number 4, I would ask you
to reconsider and look at number 2 again :)

One other thing I would explicitly recommend **against** doing is this: Some people add an instance
check inside the constructor itself, and return the existing instance from there if it exists. This
changes the semantics of the constructor, since `new MyClass()` would suddenly no longer return a
_new_ instance. That's misleading to your caller.

To protect yourself against bad practices like that, I recommend using Biome's
[`noConstructorReturn` rule](https://biomejs.dev/linter/rules/no-constructor-return/).

0 comments on commit 643092d

Please sign in to comment.