Creational Design Patterns: Why do we need them?

Edgar Almeida
ProFUSION Engineering
6 min readFeb 10, 2023

Someday, you stumble upon the necessity to add some feature to your codebase. You start to think about how to add it, and you come up with a solution. You start to implement it, and after long days and longer refactors on your codebase, it works. You have solved the problem (hooray!), but then you realize that you have to do some more refactors if you want to add another similar feature. Not so good right now, isn’t it? You think it could be done better, but you don’t know how. That is when you start to look for Design Patterns.

Design Patterns are a set of guidelines that help us solve common problems in the art of coding. Be advised that they shall not be mistaken for Algorithms since they are not a set of steps that solve a problem. In this article, I will be talking about some of the Creational Design Patterns, which are patterns intended to tackle creation problems. This will cover the problem that the pattern solves, the implementation, and the pros and cons of using it. Keep in mind that this text aims to talk about the patterns in a context of Object Oriented Programming.

Factory

The problem it solves

Imagine the following: you have a program that handles operations on Postgres databases. Your main logic is in the Postgres class, that is instantiated when the program is initialized, since you only accept Postgres. It comes a day you decide that you want to add MongoDB support too. Since most of your logic is in the Postgres class, adding MongoDB support would be a pain since you would have to do a lot of changes in your main code. What if another day you decide to add another database support? Far from ideal, right?

To the rescue comes the Factory Pattern. It suggests that you separate the logic of creating objects from the logic of using them. In this case, you would create a Database interface and two classes that implement it, Postgres and MongoDB. Then, you would create a DatabaseFactory class that would have a method returning a Database object. This way, you can create a Database object without having to know which class implements it and add new database support without having to change your main code.

Implementation

The following code shows how to implement the Factory Pattern in TypeScript:

interface Database {
initialize: () => void;
query: () => void;
}

class Postgres implements Database {
public initialize(): void {
console.log("Initializing Postgres");
}

public query(): void {
console.log("Querying Postgres");
}
}

class MongoDB implements Database {
public initialize(): void {
console.log("Initializing MongoDB");
}

public query(): void {
console.log("Querying MongoDB");
}
}

type availableDatabases = "postgres" | "mongodb";

class DatabaseFactory {
public static getDatabase(type: availableDatabases): Database {
if(type === "postgres") return new Postgres();
if(type === "mongodb") return new MongoDB();
}
}

const database = DatabaseFactory.getDatabase("postgres");
database.initialize();
database.query();

Pros and Cons

Pros:

  • You can easily add new types of objects without having to change your main code.
  • You have moved the logic of creating objects to a single place, making it easier to maintain.

Cons:

  • If you want to add a feature to one of the objects (in the example, Postgres), you would have to change the base class (Database) and therefore all the objects that implement it.

Builder

The problem it solves

Imagine the following: you have a class that represents a PersonalComputer with all its hardware components and, to create an instance of it, you would have to pass a parameter to the constructor for each hardware part, creating the object in a single shot. That's a lot of parameters. What if you don't know all of them at the same time? To help you build this PersonalComputer object, we could create a simple object at first, and then add the parameters to it. This is the Builder Pattern.

By the Builder Pattern, you create a Builder class that has a method for each parameter you want to add to the object. Each of these methods sets the related parameter and returns the instance of the Builder, which is awesome since you can make a "pipeline" of these methods. The Builder class also has a method that returns the instance of the object you are building. Then, you create a Director class that has a method which creates the object and calls the Builder methods to add the parameters. This way, you can create the object in multiple steps, and you don't have to know all the parameters at the same time. You can also easily add new "models" of objects since you only have to create a new method in the Director class.

Implementation

The following code shows how to implement the Builder Pattern in TypeScript:

class PersonalComputer {
private cpu: string = "";
private gpu: string = "";
private ram: string = "";
private storage: string = "";

public setCpu(cpu: string): void {
this.cpu = cpu;
}

public setGpu(gpu: string): void {
this.gpu = gpu;
}

public setRam(ram: string): void {
this.ram = ram;
}

public setStorage(storage: string): void {
this.storage = storage;
}

public printSpecs(): string {
return `CPU: ${this.cpu}, GPU: ${this.gpu}, RAM: ${this.ram}, Storage: ${this.storage}`;
}
}

class PCBuilder {
private pc: PersonalComputer;

public constructor() {
this.pc = new PersonalComputer();
}

public setCpu(cpu: string): PCBuilder {
this.pc.setCpu(cpu);
return this;
}

public setGpu(gpu: string): PCBuilder {
this.pc.setGpu(gpu);
return this;
}

public setRam(ram: string): PCBuilder {
this.pc.setRam(ram);
return this;
}

public setStorage(storage: string): PCBuilder {
this.pc.setStorage(storage);
return this;
}

public build(): PersonalComputer {
return this.pc;
}
}

class Director {
public static createGamingPC(builder: PCBuilder): PersonalComputer {
return builder
.setCpu("i7")
.setGpu("RTX 3080")
.setRam("32GB")
.setStorage("1TB")
.build();
}

public static createOfficePC(builder: PCBuilder): PersonalComputer {
return builder
.setCpu("i3")
.setGpu("Intel HD Graphics")
.setRam("8GB")
.setStorage("500GB")
.build();
}

public static createPCWithoutGPU(builder: PCBuilder): PersonalComputer {
return builder
.setCpu("Ryzen 5")
.setRam("16GB")
.setStorage("1TB")
.build();
}
}

const gamingPCBuilder = new PCBuilder();
const gamingPC = Director.createGamingPC(gamingPCBuilder);
console.log(gamingPC.printSpecs()); // CPU: i7, GPU: RTX 3080, RAM: 32GB, Storage: 1TB

Pros and Cons

Pros:

  • You can create the object in multiple steps, and you don’t have to know all the parameters at the same time.
  • You can easily add new “models” of objects since you only have to create a new method in the Director class.

Cons:

  • In the case of a small problem or in a case when all the parameters for the creation must be known at the same time, this pattern can be overkill and increase the complexity unnecessarily.

Singleton

The problem it solves

Imagine that you are dealing with a Sensor. You have a class that represents the Sensor that needs to be initialized before you can use the method to get the sensor data. Whenever you want to get the sensor data, you would have to create a new instance of Sensor, call the method to initialize the sensor, and then get the sensor data. This is a waste of resources since you are creating a new connection every time you want to get the sensor data. What if you could create a single instance of the Sensor connection, and then use it whenever you want to get some sensor data? This is the Singleton Pattern.

This pattern proposes creating a class that has a single instance of itself, and a method to get that instance. That method should create the instance if it doesn’t yet exist and simply returns it if it does. This way we can create a single instance of the Sensor connection, and then use it whenever we want to check the data.

Implementation

The following code shows how to implement the Singleton Pattern in TypeScript:

class Sensor {
private static instance: Sensor;
private constructor() {
console.log("Initializing sensor");
}

public static getInstance(): Sensor {
if (!Sensor.instance) {
Sensor.instance = new Sensor();
}
return Sensor.instance;
}

public getData(): void {
console.log("Returning data");
}
}

const sensor = Sensor.getInstance();
sensor.getData();

// This will not create a new instance
const sensor2 = Sensor.getInstance();
sensor2.getData();

if(sensor === sensor2) console.log('Success') // Success

Pros and Cons

Pros:

  • You can create a single instance of the object, and then use it whenever you want.
  • You gain a global access point to the object.

Cons:

  • You will need special treatment in a multithreaded environment to prevent multiple threads from accessing the getInstance method at the same time.

Conclusion

In this article, we have seen the most common Design Patterns, and how they can be used to solve common problems in software development. We have seen the Factory, Builder, and Singleton Patterns. If you are interested, I highly recommend reading Dive into Design Patterns by Alexander Shvets (2022) and accessing his website: Refactoring.Guru. Those are both great resources to learn more about Design Patterns.

Keep in mind that these and other patterns should not be used blindly, but only when they are needed. You should not start a project thinking about using some fixed pattern, instead, you should use the correct pattern when the opportunity arises. Don’t make your code more complex than it needs to be.

Sources

  1. Dive into Design Patterns by Alexander Shvets (2022) was used to get information about Design Patterns.

--

--