DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Singleton & Factory Patterns With TypeScript. Explained With Haidresser And Ice Creams Shop.
David Dal Busco
David Dal Busco

Posted on • Originally published at daviddalbusco.Medium

Singleton & Factory Patterns With TypeScript. Explained With Haidresser And Ice Creams Shop.

Of all design patterns I learned in engineering school, the singleton and factory are probably those I use the most in my day to day programming activities. Sometimes I even mix the two to double the fun 😜.

In this blog post, I will show you how to implement these patterns with TypeScript.


Singleton

In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one β€œsingle” instance (source).

A singleton is probably like your hairdresser. When you need an haircut, you do not want to go to a salon and get any new professional, even though she/he shares the same skills as yours but, you exactly want yours because she/he knows already your favorite settings βœ‚οΈ.

Such a pattern can be implemented by defining the constructor of a class as private, making it de facto not accessible outside the class declaration, and by providing only one single instance of the object which have been made static .

export class Hairdresser {
  private static instance: Hairdresser | undefined = undefined;

  private constructor() {}

  static getInstance(): Hairdresser {
    if (!Hairdresser.instance) {
      Hairdresser.instance = new Hairdresser();
    }

    return Hairdresser.instance;
  }
}
Enter fullscreen mode Exit fullscreen mode

Using the above snippet, we are not able to get any new hairdresser. Trying to instantiate a new object lead to an error.

// TS2673: Constructor of class 'Hairdresser' is private and only accessible within the class declaration.
const hairdresser: Hairdresser = new Hairdresser();
Enter fullscreen mode Exit fullscreen mode

To the contrary, accessing the instance will always return the first object which has been initialized.

const hairdresser: Hairdresser = Hairdresser.getInstance();
Enter fullscreen mode Exit fullscreen mode

Factory

In class-based programming, the factory method pattern is a creational pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created. This is done by creating objects by calling a factory method rather than by calling a constructor (source).

The factory pattern is like Eisvogel, my favorite and of course the best ice creams shop in ZΓΌrich. They sell artisanal yummy ice creams they manufacture on a daily basis with five new different flavors a day.

When you get there, you know you are going to get an ice cream but, you do not know which flavor.

In other words, you can use a factory to get objects that share an expected behavior but, which can be implemented differently.

First, it either needs an interface or an abstract class which describes the common behavior in addition to the effective implementation of the expected object which should be created by the factory.

With interface :

export interface IceCream {
  yummy(): boolean;
}

export class Strawberry implements IceCream {
  yummy(): boolean {
    return true;
  }
}

export class Chocolate implements IceCream {
  yummy(): boolean {
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

With abstract (note: using the decorator override, newly introduced with TypeScript 4.3, that indicates a method overrides the parent definition):

export abstract class IceCream {
  abstract yummy(): boolean;
}

export class Strawberry extends IceCream {
  override yummy(): boolean {
    return true;
  }
}

export class Chocolate extends IceCream {
  override yummy(): boolean {
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

Regardless of the above implementation, a related factory can be implemented. It can be in the form of a static method or a stateless function. The important thing is that depending on a parameter the desired implementation is created and returned.

With a static method:

export class Factory {
  static getIceCream(): IceCream {
    return Math.random() % 2 === 0 ? 
                new Strawberry() : new Chocolate();
  }
}
Enter fullscreen mode Exit fullscreen mode

With a function :

export const getIceCream = (): IceCream =>
  Math.random() % 2 === 0 ? new Strawberry() : new Chocolate();
Enter fullscreen mode Exit fullscreen mode

I used a random number to create either one or the other type of object. In a real world applications, the variable is often either a parameter of the factory or another option.

Requesting an object through the static method or the function will return different objects.

// Static method call 
console.log(Factory.getIceCream().yummy());

// Function call
console.log(getIceCream().yummy());
Enter fullscreen mode Exit fullscreen mode

Combined

As I said in my introduction, sometimes I like to double the fun and combine the two patterns.

For example, in DeckDeckGo, I developed such a concept to get different services according the environment (β€œlocal, staging or production”).

Applied to the above IceCream snippet, it would mean that the factory would need to keep track of the object created with a singleton .

export class SingletonFactory {
  private static instance: IceCream | undefined = undefined;

  static getInstance(): IceCream {
    if (!this.instance) {
      this.instance =
        Math.random() % 2 === 0 ? 
             new Strawberry() : new Chocolate();
    }

    return this.instance;
  }
}
Enter fullscreen mode Exit fullscreen mode

Summary

There are many patterns but, these are those I use the most often. Next time I use another type, I should probably bookmark it, it might be worth a new article πŸ˜‰.

Meanwhile, treat yourself with a yummy ice-cream of Eisvogel next time you visit Zürich 🍦.

To infinity and beyond!

David


You can reach me on Twitter or my website.

Give a try to DeckDeckGo for your next presentations!

DeckDeckGo

Top comments (4)

Collapse
totally_chase profile image
Phantz

The cost that come alongside these particular patterns should also be noted before carelessly using them. There's a certainly a lot of usability to be gained through using them, but the costs must be weighed in. Something that I don't see a lot of people doing.

The singleton pattern is the most at blame here. A pattern, whose entire premise revolves around global state, is potentially dangerous. As a industry, we've seen the major headaches caused by global state over the course of multiple decades - and I don't think I even have to explain them. As we move forward, we're trying to minimize global state for the better. But the singleton pattern seems to not get enough attention from this standpoint. And, don't get me wrong, there's a lot of attention. But the simple fact that many people are still being taught to use it without care.....should really say something.

Instead, Avoid hiding a logical dependency behind global state. In many, many cases - there's a way to simply use an explicit parameter. This is not to say that there is absolutely zero scenarios where they aren't your only choice though. In fact, a global constant binding to some object/struct/value - is perfectly fine.

The factory pattern, on the other hand, is.....weird. It's a "pattern" that aims to solve a problem that shouldn't exist in the first place. What is so hard about higher order functions? Was it really that difficult to implement half a century old concept in Java (I talk about Java here since that's the primary culprit behind the current widespread usage of this specific pattern)? Even C has the ability to use functions as data, what was so hard?

Regardless, it exists. And it's entire premise is to create a "function" depending on some configuration (arguments). Then, when that created "function" is called, it spits out some data value (termed "object" in this case) corresponding to the configuration. Why do we need a crap ton of boilerplate for this? This is a singular function, taking some arguments, and returning another function. Throw in a strong type system, and maybe currying, and wallah! you've achieved, in 5 lines, what a "pattern" would take you 50. Can we really call this a "pattern"?

In any case, my point is, while both of these patterns arguably have their uses - the alternatives should be considered.

Collapse
daviddalbusco profile image
David Dal Busco Author

Fair point! Thank you for the feedback @totally_chase .

If you develop / showcase the alternative in a blog post let me know, that would be interesting!

Collapse
totally_chase profile image
Phantz • Edited on

This post on dev.to itself is a neat little brief opener on the alternatives. Generally using FP concepts, but most of it really just boils down to higher order functions. So it should apply to JS/TS regardless. Though I must say the title is slightly misleading, but not in a predatory way. Full on pure FP langs, after all, still have a few of their own design patterns - FP doesn't eliminate the need for patterns.

After that briefer, this SO post sums up the correspondences between the GoF patterns and functional alternatives very well. But unfortunately, it never goes on to detail them too much :( - (possibly because every alternative is just a function?)

And lastly, this talk does go into some detail about exactly how to implement the alternatives. It also sums up the alternatives in a single slide! Around 3:20 minute mark. Though this one assumes some core functional concepts like currying. Thankfully, emulating currying is very simple when you have.....you guessed it! - higher order functions. JS/TS can do that. (ok that's not fully true, you also need closures but JS/TS has that too)

Thread Thread
daviddalbusco profile image
David Dal Busco Author

Awesome thanks for all the resources πŸ’ͺ

🌚 Browsing with dark mode makes you a better developer.

It's a scientific fact.