From 643092d70416ec9a1f60dcb1bb0742bea5fc863d Mon Sep 17 00:00:00 2001 From: "Arend van Beelen jr." Date: Sat, 9 Nov 2024 15:11:11 +0100 Subject: [PATCH] Singletons in JavaScript --- .../post-architecture-what-it-is-and-isnt.md | 3 + phebe/src/content/blog/post-architecture.md | 3 +- .../content/blog/singletons-in-javascript.md | 159 ++++++++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 phebe/src/content/blog/singletons-in-javascript.md diff --git a/phebe/src/content/blog/post-architecture-what-it-is-and-isnt.md b/phebe/src/content/blog/post-architecture-what-it-is-and-isnt.md index 91dc134..bae2837 100644 --- a/phebe/src/content/blog/post-architecture-what-it-is-and-isnt.md +++ b/phebe/src/content/blog/post-architecture-what-it-is-and-isnt.md @@ -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/). diff --git a/phebe/src/content/blog/post-architecture.md b/phebe/src/content/blog/post-architecture.md index 9addd6e..2fbe5ac 100644 --- a/phebe/src/content/blog/post-architecture.md +++ b/phebe/src/content/blog/post-architecture.md @@ -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/). diff --git a/phebe/src/content/blog/singletons-in-javascript.md b/phebe/src/content/blog/singletons-in-javascript.md new file mode 100644 index 0000000..8d0a320 --- /dev/null +++ b/phebe/src/content/blog/singletons-in-javascript.md @@ -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/).