Arkar Myat

Understanding Factory Design Pattern

Aug 7, 2023Arkar
Design Patterns

Discover how using factory design patterns in your code can make it more robust, less coupled, and easier to extend.

On this page

Today, I am going to talk about factory design patterns in software engineering.

What is a design pattern?

Design patterns are common solutions to known problems in software design. They are like pre-made blueprints that you can customize to solve a design problem in your code.

A pattern is not a specific piece of code, but rather a general concept for solving a particular problem. You can follow the pattern's details and implement a solution that suits solves the problem in your software.

Patterns and algorithms are often confused with each other because they both solve common problems. However, algorithms give specific instructions toward a goal, while patterns describe solutions at a higher level. The code used for the same pattern used in two different programs may not be the same.

A pattern is like a blueprint that shows the features and end result, but it is up to you to decide how to implement the code.

Creational design patterns

Creational design patterns are a group of patterns that handle how objects are created. These patterns offer different ways to create objects, which make it easier to reuse existing code and provide more flexibility.

In this article, I will cover factory design patterns, including its intentions and motivations. We'll explore what makes each pattern possible, and I'll provide the class structure and a code example to help you gain a better understanding of the pattern.

Factory design pattern

Let’s say you are building a warehouse application that handles various parcel management and deliveries. You started your application as a startup app and currently handle most of the parcel deliveries via trucks and let’s say your code may look like this first.

export class Delivery {
  private vehicle_id: string;
  constructor(id: string) {
    this.vehicle_id = id;
  }
  deliver() {
		// delivering logic
    console.log("Delivering by truck");
  }
  get_vehicle_id() {
    return this.vehicle_id;
  }
}

As your application became more popular, you started to require the use of cargo and planes for faster deliveries throughout the country. So what do you do with the code?

export type DELIVERY_TYPE = "truck" | "ship" | "plane";

export class Delivery {
  private vehicle_id: string;
  private delivery_type: DELIVERY_TYPE;
  constructor(id: string, type: DELIVERY_TYPE) {
    this.vehicle_id = id;
    this.delivery_type = type;
  }
  deliver() {
    switch (this.delivery_type) {
      case "truck": {
        console.log("Delivering by truck");
        break;
      }
      case "ship": {
        console.log("Delivering by ship");
      }
      case "plane": {
        console.log("Delivering by plane");
      }
    }
  }
  get_vehicle_id() {
    return this.vehicle_id;
  }
}

Right now, most of your code is too connected to the Truck class. If you want to add Ships and planes to the application, you will need to change the code everywhere. If you later want to add another type of transportation to the application, you will probably have to make these changes again.

So using a factory pattern you avoid creating objects directly and instead use a factory method to create them. The factory pattern provides a way to create objects without exposing the creation logic to the client and refers to the newly created object through a common interface.

In this case, you can create a DeliveryFactory that handles the creation of different delivery types based on the passed DELIVERY_TYPE. This way, you can add new types of deliveries without modifying the Delivery class.

Check out the code example below

interface Deliver {
  deliver(): void;
  get_vehicle_id(): string;
}

type DELIVERY_TYPE = "truck" | "ship" | "plane";

class Delivery implements Delivery {
  private vehicle_id: string;
  constructor(vehicle_id: string) {
    this.vehicle_id = vehicle_id;
  }
  deliver(): void {
    console.log("Delivering with vehicle id: " + this.vehicle_id);
  }
  get_vehicle_id(): string {
    return this.vehicle_id;
  }
}
class Truck_Delivery extends Delivery {
  constructor(vehicle_id: string) {
    super(vehicle_id);
  }
  deliver(): void {
    // unique truck delivery logic
    console.log("Delivering with truck id: " + this.get_vehicle_id());
  }
}

class Ship_Delivery extends Delivery {
  constructor(vehicle_id: string) {
    super(vehicle_id);
  }
  deliver(): void {
    // unique ship delivery logic
    console.log("Delivering with ship id: " + this.get_vehicle_id());
  }
}

class Plane_Delivery extends Delivery {
  constructor(vehicle_id: string) {
    super(vehicle_id);
  }
  deliver(): void {
    // unique plane delivery logic
    console.log("Delivering with plane id: " + this.get_vehicle_id());
  }
}

class DeliveryFactory {
  create_delivery(type: DELIVERY_TYPE, vehicle_id: string): Delivery {
    switch (type) {
      case "truck":
        return new Truck_Delivery(vehicle_id);
      case "ship":
        return new Ship_Delivery(vehicle_id);
      case "plane":
        return new Plane_Delivery(vehicle_id);
      default:
        throw new Error("Invalid delivery type");
    }
  }
}

class Application {
  private deliveries: Delivery[];
  factory: DeliveryFactory;
  constructor() {
    this.deliveries = [];
    this.factory = new DeliveryFactory();
  }
  order(id: string, type: DELIVERY_TYPE) {
    let new_delivery = this.factory.create_delivery(type, id);
    this.deliveries.push(new_delivery);
  }
  deliver_all() {
    this.deliveries.forEach((delivery) => {
      delivery.deliver();
    });
  }
}

As you can see in the factory method products need to implement the same interface, for example Truck_Delivery, Ship_Delivery, Plane_Delivery will need to have the same Delivery interface so that the client code that uses these interface, Application will see no difference between products returned by various sub-classes, Truck_Delivery, Ship_Delivery, Plane_Delivery.

article image

The factory design pattern provides an approach to code for interface rather than implementation and removes the instantiation of actual implementation classes from client code. This makes the code more robust, less coupled, and easier to extend.

For example, changing a DeliveryFactory class implementation will not affect the client program since it is unaware of the change. The factory pattern allows for the creation of new types of products in a program without breaking the existing application code.

However, it can be more complex than other design patterns since it often requires creating additional classes and interfaces and can be overused, leading to unnecessary complexity in the codebase.

Subscribe to my NewsLetter!

Join my web development newsletter to receive the latest updates, tips, and trends directly in your inbox.