Discover how using factory design patterns in your code can make it more robust, less coupled, and easier to extend.
Today, I am going to talk about factory design patterns in software engineering.
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 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.
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
.
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.
Join my web development newsletter to receive the latest updates, tips, and trends directly in your inbox.
Learn how to ensure only one instance of an object exists in your application for improved efficiency and consistency.
A guide to prototype pattern and how it allows for copying existing objects without relying on their classes.
Learn about the Builder pattern and how it can be used to create complex objects with ease.