Mastering Design Patterns — 03: A Comprehensive Guide to the Factory Pattern

Andrea Gernone 🫀🥷
tech bits pub
Published in
4 min readFeb 14, 2023

--

Boost Your Coding Productivity and Ace Your Interviews with the Factory Pattern

The Factory Design Pattern Hero roaming the universe.

Here we are again with one of the most well-known and flexible design patterns in Object-Oriented Programming: the Factory Design Pattern!

This design pattern facilitates object creation without revealing the object creation logic to the client, and it allows for the creation logic to be modified without affecting the clients.

You’re now an automobile factory director

Managing a vehicle factory is a good analogy for learning about the Factory Design Pattern. Various models of sedans, SUVs, trucks, and so on are manufactured in your facility on separate assembly lines. We build a factory class that generates all possible car kinds behind the scenes rather than revealing the construction logic of individual car types to the client.

Let’s illustrate the Factory Design Pattern in TypeScript with the following code snippet:

interface IAutomobile {
startEngine(): void;
drive(): void;
}

class Sedan implements IAutomobile {
startEngine(): void {
console.log("Sedan engine started");
}
drive(): void {
console.log("Driving Sedan");
}
}

class Truck implements IAutomobile {
startEngine(): void {
console.log("Truck engine started");
}
drive(): void {
console.log("Driving Truck");
}
}

class AutomobileFactory {
static getAutomobile(type: string): IAutomobile {
switch (type) {
case "Sedan":
return new Sedan();
case "Truck":
return new Truck();
default:
throw new Error("Invalid Automobile Type");
}
}
}

You can see that we have an IAutomobile interface that specifies the behavior expected of all cars. There are two concrete classes here that follow the IAutomobile specification: Sedan and Truck. Finally, we have a factory class, AutomobileFactory, with a static getAutomobile() method that, given a type argument, instantiates and returns the required object.

With our new factory class in place, we can apply it in the following way:

const sedan = AutomobileFactory.getAutomobile("Sedan");
sedan.startEngine();
sedan.drive();

const truck = AutomobileFactory.getAutomobile("Truck");
truck.startEngine();
truck.drive();

As such, this scenario illustrates how the Factory Design Pattern may be used to generate objects without revealing the object generation logic to the client. When the object generation logic is intricate and subject to change, this design shines. For instance, the creation logic might be tweaked to produce vehicles based on a variety of factors, such as a specific region or country, or a specific set of parts. You can make these changes using the Factory Design Pattern without affecting the clients who utilize the factory class.

Practical Examples

Another practical application of the Factory Design Pattern is to create log objects, such as file logs, database logs, console logs, etc., which can be created using this technique as well. An alternative to making the client aware of the specifics of creating each log type is to have them all created by a single factory class.

interface ILog {
write(message: string): void;
}

class FileLog implements ILog {
write(message: string): void {
console.log(`Writing to file: ${message}`);
}
}

class DatabaseLog implements ILog {
write(message: string): void {
console.log(`Writing to database: ${message}`);
}
}

class ConsoleLog implements ILog {
write(message: string): void {
console.log(`Writing to console: ${message}`);
}
}

class LogFactory {
static getLog(type: string): ILog {
switch (type) {
case "File":
return new FileLog();
case "Database":
return new DatabaseLog();
case "Console":
return new ConsoleLog();
default:
throw new Error("Invalid Log Type");
}
}
}

Or, you can use this information when making payment objects 💸 Payments can be made in a variety of ways, such by credit card, PayPal, bank transfer, etc. Create a factory class that handles the construction of all payment types instead of exposing the creation logic of each payment option to the client.

interface IPayment {
pay(amount: number): void;
}

class CreditCardPayment implements IPayment {
pay(amount: number): void {
console.log(`Paying with credit card: ${amount}`);
}
}

class PayPalPayment implements IPayment {
pay(amount: number): void {
console.log(`Paying with PayPal: ${amount}`);
}
}

class BankTransferPayment implements IPayment {
pay(amount: number): void {
console.log(`Paying with bank transfer: ${amount}`);
}
}

class PaymentFactory {
static getPayment(type: string): IPayment {
switch (type) {
case "CreditCard":
return new CreditCardPayment();
case "PayPal":
return new PayPalPayment();
case "BankTransfer":
return new BankTransferPayment();
default:
throw new Error("Invalid Payment Type");
}
}
}

LogFactory and PaymentFactory are two factory classes that you may use to generate Log instances and Payment objects, respectively. There is a static getLog() or getPayment() method in each factory class that, depending on the type of argument, simply generates and returns the requested object.

After establishing our factory classes, we may leverage them in the following way:

const fileLog = LogFactory.getLog("File");
fileLog.write("This is a log message");

const databaseLog = LogFactory.getLog("Database");
databaseLog.write("This is a log message");

const consoleLog = LogFactory.getLog("Console");
consoleLog.write("This is a log message");

const creditCardPayment = PaymentFactory.getPayment("CreditCard");
creditCardPayment.pay(100);

const paypalPayment = PaymentFactory.getPayment("PayPal");
paypalPayment.pay(100);

const bankTransferPayment = PaymentFactory.getPayment("BankTransfer");
bankTransferPayment.pay(100);

We are utilizing the LogFactory and PaymentFactory classes as factories to generate Log and Payment objects, respectively. By hiding the object generation mechanism from the client, we may construct objects in a structured and versatile manner.

Obviously, real-world projects will be more complex than the ones given here. However, we can still use them to learn the notion and apply it in other contexts.

To sum up, the Factory Design Pattern is a powerful and flexible design pattern that facilitates the efficient and adaptable production of items. I suggest looking into this topic further and searching for code samples online if you’re interested in learning more and implementing it in your projects.

To that end, I write this post with the intention that you’ll get a deeper appreciation for the Factory Design Pattern and its practical use. The Abstract Factory Pattern, a refinement of this one, will be discussed in the subsequent article.

May you all have a brilliant time writing code! 🚀

Cheers! 🍻

Andrea

--

--

Andrea Gernone 🫀🥷
tech bits pub

I am a full stack professional software developer at @apuliasoft. Based in Italy and passionate about anything that enriches my soul. Also I love to eat