Singleton - Design Patterns meet the Frontend
As an Angular developer I work with Singletons every day, in the shape of Injectables
. However, when I sat down to write about Singletons I did a double take.
NOTE: Code samples in this article will be using TypeScript (3.7) and two examples at the end will use Java.
Let's break this down into a few questions:
- π€ What constitutes a Singleton?
- β Why are they used in Angular?
- π What advantages do we get from Singletons?
- π£ Why is there a debate over whether they are an Anti-Pattern or not?
In this article, I'll attempt to shed light on some of these questions.
π€ What constitutes a Singleton?
We'll start with the definition of a Singleton:
The restriction of the instantiation of a class to a single instance.
In other words, any time we instantiate a class, we'll only ever get the first actual instance of that class.
These Singletons can implement interfaces, be passed around methods as arguments and can be polymorphic.
However, every time we use the class, it will be the same object that we first created. The code sample below may shine more light on the situation:
class UserService {
// We need a static reference to the class itself
static instance: UserService;
constructor() {
// If the static reference doesn't exist
// (i.e. the class has never been instantiated before)
// set it to the newly instantiated object of this class
if (!UserService.instance) {
UserService.instance = this;
}
// Return the static instance of the class
// Which will only ever be the first instance
// Due to the if statement above
return UserService.instance;
}
}
And that's it! Creating a Singleton is as simple as that! π₯π₯π₯
But what exactly does that mean? How will this affect our programming and where do we find value in this?
Well, a Singleton can be used to store shared state. You can instantiate your Singleton in your app, change some data within it and it will be reflected across your app.
β οΈ Doing this could be dangerous if you don't think about it carefully!
Let's modify our sample above to illustrate this:
// We'll start by modifying our class
class UserService {
static instance: UserService;
user?: { name: string };
constructor() {
if (!UserService.instance) {
UserService.instance = this;
}
return UserService.instance;
}
logName() {
console.log(`Hi, my name is: ${this.user?.name}`);
}
}
// Now we will use our service to handle
// Two different users of our app
const myService = new UserService();
myService.user = { name: 'Colum' };
const differentService = new UserService();
differentService.user = { name: 'John' };
myService.logName();
differentService.logName();
The outcome of the above sample is:
Hi, my name is John
Hi, my name is John
π€π€ But didn't we set myService.user
to have a name of Colum
?
We did, but that was before we set differentService.user
to have a name of John
.
By doing so, as there can only ever be one instance of the UserService
, we actually changed the name of the single user
object from Colum
to John
.
This idea of being able to share state can be very useful at times, for example, if you want to keep track of the current logged in user.
Singletons can be lazy loaded, potentially enhancing the startup performance of an app, however, on multi-threaded languages, implementation of a lazy loaded Singleton can cause issues of multiple instantiations across multiple threads, breaking the pattern, if not declared to be synchronized across threads.
β So, why are they used in Angular?
Angular's Dependency Injection relies on Injectables
. Dependency Injection itself is also a Design Pattern, but it is relevant here as in this context, it makes use of the Singleton Pattern.
Angular apps consist usually with multiple modules (NgModules
), but at their minimum have one root module which has a list of providers
required for the app. These are then injected into classes that require them.
Let's take a look at an Angular Component that seeks a LogService
to be injected into it.
@Component({...})
export class MyComponent implements OnInit {
constructor(private readonly logService: LogService) {}
ngOnInit() {
this.logService.log("Created component");
}
}
The first thing to take from this, is that the constructor
of our component takes an argument of type LogService
.
Secondly, without creating an instance of this class, we use it in our ngOnInit()
method. That is because Angular will instantiate this class, hold it in memory, and pass it into any class that requires it.
By doing so, Angular is also utilizing the Singleton Pattern, as it only ever instantiates a module's providers once, and passes the reference to this instance into any class depending on it.
π What advantages do we get from Singletons?
As mentioned before, we can share state with different areas within our app by using the Singleton Pattern, and in Angular, it is made much simpler thanks to their Dependency Injection system.
There are use-cases involving single access to a data store, keeping track of active items in a shopping cart, a single source of truth etc.
β οΈ Be advised that Singletons are not intended to replace any State Management system you are currently using.
However, it also allows us to create logic services, a class of methods that can be reused in multiple situations to provide the same logic, that can be instantiated once only, helping to aid code reusability and maintainability without having a high impact on memory usage or performance of apps.
Use cases for these are data services which call API Endpoints, services that will make and return decisions based on arguments passed to their methods etc.
π£ Why is there a debate over whether they are an Anti-Pattern or not?
There is an argument that Singletons promote bad design within code by actively working against the dependency inversion (DI) principle. Classes that do accept interfaces as arguments in their constructor, can simply accept a constant/final instantiated object as a parameter.
This argument generally arises from Developers who work with a strongly-typed language or when they have complete control over their DI system. See the Java example below for more clarity:
class DataService implements IDataService {...}
class UserToolbar implements IToolbar {
public UserToolbar(IDataService dataSerice) {...}
}
class UserPage implements IPage {
public UserPage(IDataService dataSerice) {...}
}
class App implements IApp {
public App(IToolbar toolbar, IPage page) {...}
}
public class Startup {
// final keyword prevents this from ever being overwritten
// meaning this will only ever be the original object
public final IDataService dataService = new DataService();
public Startup() {
IToolbar toolbar = new UserToolbar(dataService);
IPage page = new UserPage(dataService);
IApp app = new App(toolbar, page);
}
public static void main(String[] args) }{
Startup startup = new Startup();
}
}
As we can see from above, the developer has complete control over the structure of the app and can decide for themselves which implementations of the interfaces they would like to use, can instatiate them at the start-up of their app, and pass them into the dependent classes.
If the Singleton Pattern was to be used here, only one possible implementation of the data service could be used. See the changes in the code below:
// Note the singleton setup of the DataService class
class DataService {
public static final instance;
public DataService() {
if(!DataService.instance) {
DataService.instance = this;
}
return DataService.instance;
}
}
// Note that this class no longer accepts an IDataService interface
// As an argument to the constructor preventing any implentation to
// be used for this class
class UserToolbar implements IToolbar {
DataService dataService = new DataService();
public UserToolbar() {...}
}
class App implements IApp {
public App(IToolbar toolbar) {...}
}
public class Startup {
// Note that this class no longer needs to instantiate the DataService
// Class as it is handled in any class that needs it
public Startup() {
IToolbar toolbar = new UserToolbar();
IApp app = new App(toolbar);
}
public static void main(String[] args) }{
Startup startup = new Startup();
}
}
Whilst the code above is arguably smaller, it does break the Dependency Inversion principle and creates tighter coupling between the classes that rely on the Singleton and the Singleton itself.
βββ However
It is worth noting that Angular gets around this issue with the use of Injection Tokens which is a slightly more advanced topic. It allows you to provide concrete implementations at NgModule level rather than coupling your components and services to your Singletons.
πππ
Hopefully you learned a bit (more?) about Singletons from this article, some of their use-cases and their implementation in Angular.
If you have any questions, feel free to ask below or reach out to me on Twitter: @FerryColum.
Top comments (1)
hey, nice post, I want to ask you your opinion about the old school static getInstance() / constructor(){if(!allowInstanciation)throw(new Error())} model