Creational Design Patterns using Typescript

Yatin M.
Technology20 mins read
creational-design-patterns-min.webp

Creational Design Patterns using Typescript

Creational Design Patterns are the category of design patterns that deal with object creation mechanisms. These are particularly important when the developer needs to incorporate some types of constraints on the creation of objects, for eg: having a single instance of an object or decoupling the instance created from where in the code it is used. This particularly helps resolve future complexity in software design.

By the end of this article, you’d have a clear idea, what are the creational design patterns, when are they applicable and which circumstance would require what type of Creational Design Pattern.

Primarily we have five types of creational design patterns:

  1. Singleton
  2. Prototype
  3. Factory Method
  4. Builder
  5. Abstract Factory

These are the types discussed in GoF Design Patterns. In addition to these is another important creational design pattern —Object Pool— used in the case of performance-boosting applications for complex class instantiations.

Let’s understand each of these design patterns (excluding Object Pool, which we’ll cover in another blog post) with practical examples. We’ll use Typescript as the language and favour practical examples over the theoretical ones you usually find online, involving classes such as box, animal, etc.


Singleton Design Pattern

The Singleton Design Pattern derives its origin from the mathematical concept of Singleton sets. This pattern restricts the instantiation of a class to just a single instance. It should be used very selectively and rarely since it’s mostly non-essential. One can even argue as to why we need a separate class at all, and not just a single variable or a set of global variables.

Problem

In most cases, the developer should avoid global variables and instances. But for cases such as logging — where logging is required with each function and does not change the state of the code — there is no need for instantiation of a class in every method of the codebase. So, to avoid unnecessary repetition and for performance gains, the Singleton pattern can be utilized.

Let’s consider a logger implementation as follows:

class Logger {
    logError(log: string): void {
        console.log(log);
    }
}

Logger Not A Singleton

To utilize the above logger, a sample implementation can instantiate the Loggerclass and utilize the logger.logError method as seen in the following Gist.

const getMonth = (date: string): number => {
    let month: number;
    try {
        month = new Date(date).getMonth();
        if(isNaN(month)) {
            throw 'Invalid date';
        }
    } catch(err) {
        const logger = new Logger();
        logger.logError(`Error while instantiating date: ${date} (${String(err)})`);
    }
    return month;
}

const getYear = (date: string) => {
    let year: number;
    try {
        year = new Date(date).getFullYear();
        if(isNaN(year)) {
            throw 'Invalid date';
        }
    } catch (err) {
        const logger = new Logger();
        logger.logError(`Error while instantiating date: ${date} (${String(err)})`);
    }
    return year;
}

const main = () => {
    const dateFromApi = 'Fri Nov 27 2020 10:53:42 GMT+0530 (India Standard Time)';
    getMonth(dateFromApi);
    const dateFromApi2 = 'zz'
    getMonth(dateFromApi2);
}

Utilizing Logger to log errors

The above code works well, but when you expand it to hundreds of different functions and classes, the unnecessary initializations suddenly start to feel redundant. One solution can be to inject the logger object everywhere. But again, it adds on as an unneeded parameter. apart from if the initializer is slow and complex, multiple initializations will start to have a compounding effect in a negative direction.

Solution

The best solution here is the Singleton Pattern. This is particularly useful in cases where the need for a global object is most desired. Let’s take a look at the implementation of the Logger class as a Singleton.

class Logger {
    private static singleton: Logger;
    private constructor () {
        // Any initialization logic
    }

    static getInstance() {
        if (Logger.singleton) {
          return Logger.singleton;
        }
        Logger.singleton = new Logger();
        return Logger.singleton;
    }

    logError(log: string): void {
        console.log(log);
    }
}

Logger class as a Singleton

If you look at Line 7, the getInstance returns the object of the class and it’s a static method. This is what creates the Singleton class. It restricts the creation of more than a single object of the class and returns the same object every time an instance is needed. Also, the constructor is private, restricting the implicit creation of a duplicate object.

This new Singleton class can be used as seen in the next Gist.

const getMonth = (date: string): number => {
    let month: number;
    try {
        month = new Date(date).getMonth();
        if(isNaN(month)) {
            throw 'Invalid date';
        }
    } catch(err) {
        Logger.getInstance().logError(`Error while instantiating date: ${date} (${String(err)})`);
    }
    return month;
}

const getYear = (date: string) => {
    let year: number;
    try {
        year = new Date(date).getFullYear();
        if(isNaN(year)) {
            throw 'Invalid date';
        }
    } catch (err) {
        Logger.getInstance().logError(`Error while instantiating date: ${date} (${String(err)})`);
    }
    return year;
}

const main = () => {
    const dateFromApi = 'Fri Nov 27 2020 10:53:42 GMT+0530 (India Standard Time)';
    getMonth(dateFromApi);
    const dateFromApi2 = 'zz'
    getMonth(dateFromApi2);
}

Usage of Singleton Class

Summary

The Singleton Pattern on the surface seems like a way to use Global variables. But it has more implications when you start looking into it deeply. Firstly, the Singleton Pattern helps to have globally accessible actions instead of just variables. This is particularly useful in cases such as the above Logger. Other examples include cases like Database connection pools where you won’t need multitudes of connections.

One criticism this approach faces is that the global variables to which the Singleton Pattern is widely compared have mutable characteristics. But this should not be a problem if we keep in mind the fact that we do not want to change our unit test cases when applying the Singleton pattern.


Prototype Design Pattern

The Prototype Design Pattern is used when the creation of a new object is complex or expensive and a better approach is to clone the existing object.

Problem

The prototype pattern is typically used in cases where the creation of a new object is expensive or mutability is an issue. Let’s take a simple example to understand the problem.

For example, in a typical text editor or similar application, we have a feature to find a word in a blob of text.

import loremIpsum from './lorem';
class WordOccurences {
    occurrences: Array<number>;

    constructor (text, find) {
        let counter = 0;
        const allWords = text.split(" ");
        const wordCount = allWords.length;
        this.occurrences = [];
        while( wordCount > counter) {
            if (find === allWords[counter]) {
                this.occurrences.push(counter);
            }
            counter += 1;
        }
    }

    getOccurrences() {
        return this.occurrences;
    }
}

const main = () => {
    console.log((new Date()).toISOString());
    const wordOccurrences1 = new WordOccurences(loremIpsum, 'eros');
    console.log(wordOccurrences1.getOccurrences());
    console.log((new Date()).toISOString());
    const wordOccurrences2 = new WordOccurences(loremIpsum, 'eros');
    console.log(wordOccurrences2.getOccurrences());
    console.log((new Date()).toISOString());
}

main();

Word Occurrences Object Creation

From the above example, the first object creation takes 32 milliseconds, while the second one takes only 5–10 milliseconds. This is because of the optimizations provided by the compiler.

Solution

Since only a copy is needed to initialize, a better approach is to use a clone of the object. This can easily be done using the “Object.create” method.

import loremIpsum from './lorem';
class WordOccurences {
    occurrences: Array<number>;

    constructor (text, find) {
        let counter = 0;
        const allWords = text.split(" ");
        const wordCount = allWords.length;
        this.occurrences = [];
        while( wordCount > counter) {
            if (find === allWords[counter]) {
                this.occurrences.push(counter);
            }
            counter += 1;
        }
    }

    getOccurrences() {
        return this.occurrences;
    }
}

const main = () => {
    console.log((new Date()).toISOString());
    const wordOccurrences1 = new WordOccurences(loremIpsum, 'eros');
    console.log(wordOccurrences1.getOccurrences());
    console.log((new Date()).toISOString());
    const wordOccurrences2 = Object.create(wordOccurrences1);
    console.log(wordOccurrences2.getOccurrences());
    console.log((new Date()).toISOString());
}

main();

Prototype Design Pattern Implementation in Typescript

Looking at line 28, instead of instantiation -- the clone is created. This optimizes the code to generate a new object to less than 1ms, giving us an optimization of more than 400%+.

Summary

This pattern is generally not utilized in normal web-applications but becomes crucially important in cases where new object creation is complex or expensive. Typical cases such as instances where object creation is dependent on some network call that takes time might make use of Prototype Pattern. Other use cases include Gaming applications where items such as multiple new levels or items in the game such as cubes, doors etc. are created by cloning the original object.


Factory Method Design Pattern

The Factory Design Pattern deals with object creation separately from the client calling code. This is usually done to facilitate the DRY principle. Let’s just discuss this with an example.

Problem:

Suppose, there is a cybersecurity SAAS available with 2 plans: One is free with advertisements, and the second is the Pro plan with no advertisements. Since we’re pro-OOP — we start by creating aMemberclass and extend it for our cases.

interface IUserPersonalInfo {
  name: string;
  address: string;
}

class Member {
  name: string;
  address: string;
  paid: boolean;
  showAds: boolean;
  constructor(personalDetails: IUserPersonalInfo) {
    // setup name etc.
  }
  notify() {}
  register() {
    // Perform some DB Call; Register to some 3rd party
    console.log("Registered");
  }
}

class ProMember extends Member {
  constructor(personalDetails: IUserPersonalInfo) {
    super(personalDetails);
    this.paid = true;
    this.showAds = false;
  }
}

class FreeMember extends Member {
  constructor(personalDetails: IUserPersonalInfo) {
    super(personalDetails);
    this.paid = false;
    this.showAds = true;
  }
}

Member Class extended by ProMember and FreeMember Classes

Now it’s quite simple to use these classes and create members based on the type i.e. “free” or “pro”.

const setup = (type: string, userDetails: IUserPersonalInfo) => {
  let member: Member;
  switch (type) {
    case "free":
      member = new FreeMember(userDetails);
    case "pro":
      member = new ProMember(userDetails);
  }
  member.register();
  member.notify();
};

Use Member Classes on Client-Side

This code works fine and goes ahead with decent use of OOPs we know of. However, problems start cropping up when we need to add more cases to the above. Say the next requirement is to create a premium membership as well. Moreover, this function is doing multiple things at once — creating, registering and notifying. Most importantly, if the code is duplicated at multiple places, we’ve let go of the DRY principle.

Solution

Solving this is simple and can be thought of as extracting the repeated code into a common reusable function. This, in simple terms, is what the Factory method is doing. It extracts the common logic for object instantiation with reusable code.

class MembershipFactory {
  static createMembership(
    type: string,
    userDetails: IUserPersonalInfo
  ): Member {
    let member: Member;
    switch (type) {
      case "free":
        member = new FreeMember(userDetails);
        break;
      case "pro":
        member = new ProMember(userDetails);
        break;
      default:
        member = new FreeMember(userDetails);
    }
    return member;
  }
}

const setup = () => {
  const member: Member = MembershipFactory.createMembership("free", {
    name: "Tim",
    address: "Somewhere",
  });
  member.register();
  member.notify();
};

Factory Method

In the above example, we simply create a MembershipFactory class and add a static method encapsulating all the logic of the Member creation into a Factory class. Object creation is delegated and code duplication is avoided. The resulting code has only 1 place of change.

Summary

This pattern can simply be called “Simple Factory” and in some cases, it isn’t even described as a pattern since it’s simply an encapsulation of repeated code. Either way, it comes in pretty handy in such cases, regardless of whether or not we call it a pattern.

There are a few enhancement requirements that are not handled well by the Factory Method Pattern, for which we need to understand the applicability of the “Abstract Factory Method”.


Abstract Factory Method Design Pattern

Abstract Factory is more of an abstraction on the above-discussed Factory Method. With the Abstract Factory Method, we achieve encapsulation on object instantiation using abstract Factory classes, which results in the creation of objects of a similar theme.

Problem:

Let’s extend the example discussed previously with the Factory Method. Say we have a new requirement to have additional types of members but on similar themes of “Free” and “Pro”. Suppose the company was previously situated in the USA, and now plans to extend the product to European markets. We could use the same classes, but there are many more checks to be handled for European markets, like compliance. These types of object instantiations are already solved using the Abstract Factory Method pattern.

Solution

Before jumping into the solution, let’s quickly define some terms that are generally found in codebases or books to describe the Abstract Factory Design Pattern.

  1. Abstract Factory — Self Explanatory
  2. Concrete Factory — Implementation of Abstract Factory
  3. Abstract Product — Contract for Class derived from the Factory
  4. Concrete Product — Resulting Product from Factory Class Implementation

To solve this let’s take a bottom-up approach, understanding the Product class first, and then the abstraction around Factory Class.

Remember the Member class from the Factory Method above. That’s our Product class. Let’s now create aEuropeanMember with an agreement field.

class EuropeanMember {
  agreementSigned: boolean;
}

class EuropeanProMember extends ProMember implements EuropeanMember {
  agreementSigned: boolean;

  constructor(personalDetails: IUserPersonalInfo) {
    super(personalDetails);
    // Knight the Pro User
    this.name = `Sir ${this.name}`;
    this.agreementSigned = true;
  }
}

class EuropeanFreeMember extends FreeMember implements EuropeanMember {
  agreementSigned: boolean;
  constructor(personalDetails: IUserPersonalInfo) {
    super(personalDetails);
    this.agreementSigned = true;
  }
}

European Member Class - Product Class

Previously,Memberserved as the abstract product,ProMember and FreeMember served as the default ConcreteProduct (or Concrete Product for USA in our case), EuropeanProMember and EuropeanFreeMember served as the Concrete Product classes for European markets.

Next, we need Factories to instantiate these concrete products. But before that, we need to refactor and create contracts for our Abstract Factory.

abstract class MembershipFactory {
  abstract createMembership(
    type: string,
    userDetails: IUserPersonalInfo
  ): Member;
}

Abstract Factory

Now, finally, let’s add our Concrete Factory classes. We will be creating 2 concrete classes, USAMembershipFactory and EuropeanMembershipFactory that comply with the above Abstract Factory i.e. MembershipFactory.

Using their own implementations, they can create particular types of members.

class USAMembershipFactory extends MembershipFactory {
  createMembership(type: string, userDetails: IUserPersonalInfo): Member {
    let member: Member;
    switch (type) {
      case "free":
        member = new FreeMember(userDetails);
        break;
      case "pro":
        // check Social Security Identification logic...
        member = new ProMember(userDetails);
        break;
      default:
        member = new FreeMember(userDetails);
    }
    return member;
  }
}

class EuropeMembershipFactory extends MembershipFactory {
  createMembership(type: string, userDetails: IUserPersonalInfo): Member {
    let member: Member;
    // Check for European Compliance....
    switch (type) {
      case "free":
        member = new EuropeanFreeMember(userDetails);
        break;
      case "pro":
        // Expiry Works on Financial Year..
        member = new EuropeanProMember(userDetails);
        break;
      default:
        member = new FreeMember(userDetails);
    }
    return member;
  }
}

Concrete Factory Classes

This makes it easier to achieve independent Product classes. The implementation is quite trivial in our case but becomes particularly useful in the case of multiple and complex business requirements.

Summary

Primarily Abstract Factories are used in conjunction with the Factory Method and are frequently used in codebases. The key point to keep in mind is that whenever there is a need for object instantiation of similar themes without specifying the concrete classes — Abstract Factory Method might be used.

Frequently, it can be encountered in implementations where we get an iterable that may be of the typelist, or a custom class (you can iterate or get the length of the object) that conforms to the abstract product class and is a result of an implementation of an Abstract Factory.


Builder Design Pattern

The Builder Design Pattern is used for separating concerns of object creation from its UI representation.

Problem

Let’s take a typical example of an API. In our case, this API is built for a chess website. We have 2 typical classes/models for:

  1. Players and 2. Match
class Player {
    name: string;
    // And possibly 10 other fields

    constructor(name: string) {
        this.name = name;
    }
}

class ChessMatch {
    id: number;
    player1: Player;
    player2: Player;
    startTime: Date;
    stopTime: Date;
    isLive?: boolean;

    constructor(id, player1, player2, startTime, stopTime) {
        this.id = id;
        this.player1 = player1;
        this.player2 = player2;
        this.startTime = startTime;
        this.stopTime = stopTime;
    }
}

Players and ChessMatch Classes

Next, we try to fetch data using an API call and encapsulate the data using both Players and ChessMatch classes.

const fetchMatchDetailsFromDB = (id: number) => {
    // A DB Call
    return new ChessMatch(id, new Player("Bobby"), new Player("Vishy"), new Date(), new Date())
}

const matchDetails = (id: number) => {
    return fetchMatchDetailsFromDB(id);
}

const main = () => {
    matchDetails(1);
}

Fetch Data from DB and return Chess Match with a particular ID

The fetchDetailsFromDB the method can be thought of as a function that makes use of a typical ORM and returns the data in a specified model format i.e. in ChessMatch object.

Normally, we rarely need data in the same format in UI as produced by the backend. Let’s say that in the above example, we need an isLive flag in the UI that is true when the match is about to start in 5 minutes till the end of the match.

Solution

There are many approaches that come to mind when solving this particular problem. We shall discuss a few in this section:

Approach One:If it’s needed in the UI, then set it in the UI

This approach, though faster and seemingly obvious, hinders scalability. Think about a future requirement that needs the same output in both a web-app and a mobile app, saving 1 implementation effort doubles the work on the frontend. And what if the same API is being used in multiple places in both the web and mobile apps? With each increase in API usage, your incremental effort doubles.

Approach Two: Create a transformer/service layer

This is generally seen in many codebases in the backend where there is a mix of object-oriented and functional approaches being followed.

const transformMatchData = (matchInfo: ChessMatch): ChessMatch => {
    const now = new Date();
    matchInfo.isLive = matchInfo.startTime <= now && matchInfo.stopTime >= now;
    return matchInfo;
}


const matchDetails = (id: number) => {
    // First Approach
    const matchInfo = fetchMatchDetailsFromDB(id);
    console.log(transformMatchData(matchInfo));
}

const main = () => {
    matchDetails(1);
}

main();

2.A - Transformer for API data

This approach is generally seen in more non-object-oriented/functional code and has developed into a pattern of its own.

Another way to make it more of an object-oriented approach (which does not reap any benefits, in my opinion) is the following —

class ChessMatchService {
    private match: ChessMatch;

    constructor(match: ChessMatch) {
        this.match = match;
    }
    transformMatchData = () => {
        const now = new Date();
        this.match.isLive = this.match.startTime <= now && this.match.stopTime >= now;
        return this.match;
    }
}

const matchDetails = (id: number) => {
    // Or a second Approach
    const matchService = new ChessMatchService(matchInfo);
    console.log(matchService.transformMatchData());
}

const main = () => {
    matchDetails(1);
}

main();

2.B - Object-oriented Service Layer

The first transformer is basically transformed into a class method in the second approach and serves more or less the same purpose. Any of these approaches are incrementally better than the first approach. Personally, I would pick the first approach over the second, since it adds no unnecessary object-oriented abstraction.

Approach Three: The Builder Pattern

There is a better solution. When thinking about scalability and future requirements, we need to presume another set of requirements for the future.

For example, there are now 3 sets of new requirements:

  1. Match Object with isLive status on match Detail Page.
  2. Match Object for Navbar without isLive status, and with only player names.

Let’s take a look at the basic implementation and how it would for both the above API’s.

const navbarApi = (id: number) => {
  const navbarMatchBuilder = new NavbarChessMatchBuilder();
  const chessMatchDirector = new ChessMatchDirector(navbarMatchBuilder, id);
  chessMatchDirector.construct();
  return navbarMatchBuilder.getResult();
};

Navbar Chess Match API Function

const detailPageApi = (id: number) => {
  const detailMatchBuilder = new DetailChessMatchBuilder();
  const chessMatchDirector = new ChessMatchDirector(detailMatchBuilder, id);
  chessMatchDirector.construct();
  return detailMatchBuilder.getResult();
};

Detail Chess Match API Function

Looking at the above code with both API functions, we first instantiate the builder for each builder —

NavbarChessMatchBuilder and DetailChessMatchBuilder respectively.

So the flow for a typical builder pattern is as follows:

  1. Director assembles a particular instance for the resulting class (in our case ChessMatch instance)
  2. Director then delegates the rest of the building to the particular Builder class (NavbarChessMatchBuilder and DetailChessMatchBuilder)
  3. The Builder can give the final result based on its particular specification.

Let’s take a closer look at the Director class for our particular example:

class ChessMatchDirector {
  private chessMatchBuilder: ChessMatchBuilder;
  constructor(chessMatchBuilder: ChessMatchBuilder, id: number) {
    this.chessMatchBuilder = chessMatchBuilder;
    this.chessMatchBuilder.chessMatch = new ChessMatch(
      id,
      new Player("Bobby"),
      new Player("Vishy"),
      new Date(),
      new Date()
    );
  }

  construct() {
    this.chessMatchBuilder.build();
  }
}

The Director Class Implementation

The Director class has only 2 specific functions:

  1. Assemble the initial result class.

This is done in **line 5 **with new ChessMatch instantiated.

2. Delegate the rest of the building to a particular builder class.

This is done using the construct function.

Lastly, let’s take a closer look at the Builder classes which are extended from the abstract ChessMatchBuilder in our use-case.

abstract class ChessMatchBuilder {
  chessMatch: ChessMatch;
  abstract build();
  abstract getResult();
}

class NavbarChessMatchBuilder extends ChessMatchBuilder {
  build(): void {
    // Add some other details for outer joins..
  }
  getResult = () => {
    return {
      player1: this.chessMatch.player1.name,
      player2: this.chessMatch.player2.name,
      startTime: this.chessMatch.startTime,
    };
  };
}

class DetailChessMatchBuilder extends ChessMatchBuilder {
  build(): void {
    const now = new Date();
    this.chessMatch.isLive =
      this.chessMatch.startTime <= now && this.chessMatch.stopTime >= now;
  }
  getResult = () => {
    return this.chessMatch;
  };
}

Builder Class Implementation

In DetailChessMatchBuilder we needed the isLive status that has been added inline 23. While we have no such requirement for the match status on Navbar, we do need lesser information from player objects (i.e. only 3 keys). The result is modified accordingly on** line 11**.

Summary

As you can see from the previous example, the Builder pattern helps us in the separation of concerns for object instantiation from its representation. The object-oriented way of transformer is far more cleaner than it’s non-object-oriented counterparts.

The need for a Director class is not compulsory but having similar names and a structure helps all the developers to quickly get in sync with what’s happening inside huge codebases.

Conclusion

In this part, we’ve summarised all of GoF Creational Design Patterns using practical examples and clear explanation with Typescript. Creational Design Patterns cannot be ignored in the case of applications ranging from medium to large backends with decent complexity and scalability problems. These help us stand on the shoulders of the giants who have already solved this problem with extreme diligence.

Category
Technology
Published On
21 Feb, 2021
Share

Subscribe to our tech, design & culture updates at Proximity

Weekly updates, insights & news across all teams

Copyright © 2019-2023 Proximity Works™