The Factory Pattern In TypeScript
The Factory Design Pattern is a type of creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.
This pattern is commonly used in applications that handle the creation of complex objects, when we want to decouple the object creation from the client code that uses these objects.
Classic Implementation
Suppose we're building a car manufacturing program where each car has a model and a year of production. We have different types of cars like Sedan, SUV, and Hatchback.
First, let's define a base Car class:
abstract class Car {
constructor(public model: string, public productionYear: number) {}
abstract displayCarInfo(): void;
}
Next, let's create the concrete Car types: Sedan, SUV, and Hatchback.
class Sedan extends Car {
displayCarInfo() {
console.log(
`This is a Sedan. Model: ${this.model}, Production Year: ${this.productionYear}`
);
}
}
class SUV extends Car {
displayCarInfo() {
console.log(
`This is an SUV. Model: ${this.model}, Production Year: ${this.productionYear}`
);
}
}
class Hatchback extends Car {
displayCarInfo() {
console.log(
`This is a Hatchback. Model: ${this.model}, Production Year: ${this.productionYear}`
);
}
}
We've defined the types of cars, but how do we create them? Here's where the Factory pattern comes into play. We define a CarFactory that creates a car based on the type input:
class CarFactory {
public createCar(type: string, model: string, productionYear: number): Car {
switch (type) {
case "Sedan":
return new Sedan(model, productionYear);
case "SUV":
return new SUV(model, productionYear);
case "Hatchback":
return new Hatchback(model, productionYear);
default:
throw new Error("Invalid car type");
}
}
}
Now, let's use our CarFactory:
const carFactory = new CarFactory();
const sedan = carFactory.createCar("Sedan", "Camry", 2023);
sedan.displayCarInfo();
// This is a Sedan. Model: Camry, Production Year: 2023
const suv = carFactory.createCar("SUV", "RAV4", 2023);
suv.displayCarInfo(); //
// This is an SUV. Model: RAV4, Production Year: 2023
const hatchback = carFactory.createCar("Hatchback", "Corolla", 2023);
hatchback.displayCarInfo();
// This is a Hatchback. Model: Corolla, Production Year: 2023
As you can see, the Factory pattern allows us to create different types of Car objects without directly instantiating classes. Instead, we use the CarFactory to create objects, making it easier to add new Car types without modifying the client code.
Time To Transition From JavaScript To TypeScript
Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.
SEE COURSE DETAILSWhy Abstract Class Instead Of Interface?
In this specific example of implementation of the Factory Pattern, the
advantage of using anabstract class
over an interface
is the ability to
include a constructor and the ability to define
default behavior that can be reused by subclasses.
Constructor: An interface does not have a constructor. This means you can't control the construction of the objects that implement it. On the other hand, abstract classes can have constructors, allowing you to define and enforce a certain way of creating objects. In this case, each
Car
must be created with amodel
and aproductionYear
, which can be enforced by the abstractCar
class constructor.Implementation: An interface just defines a contract but doesn't provide any implementation. Abstract classes can provide some default behavior that can be shared across multiple subclasses. In this case, the
Car
class could potentially provide some default methods that are common to all cars.
When To Use Factory Pattern
The Factory Method pattern is often used in situations where a class cannot anticipate the type of objects it needs to create.
Here are some signs that might indicate that a Factory Method pattern could be appropriate:
Uncertain Object Types: If your software is supposed to create different types of objects, and you don't know what these objects will be until runtime, you may need a Factory Method.
Similar Classes: If you're dealing with a large number of classes that share a common superclass and you often need to instantiate one of these classes, but you don't know ahead of time which one you'll need to instantiate, a Factory Method can be useful.
Pluggability and Flexibility: If you are developing a library and want to provide a way for users to extend your library with their own classes, a Factory Method can provide a standardized way for users to plug in their own classes.
Replacing Direct Object Construction: If you see code that's directly constructing instances of a class, this might be a code smell suggesting that a Factory Method could be used. Directly constructing objects can make code more brittle, harder to test, and less flexible.
Complexity Hiding: When object creation is complex or involves a lot of logic (for example, setting up and connecting several different objects), a Factory Method can hide this complexity and provide a simpler interface for object creation.
Conditional Object Creation: If your code involves conditional creation of objects based on certain parameters or environmental conditions, a Factory Method can encapsulate this conditional logic and make your code easier to read and maintain.
As with all design patterns, it's important to consider the trade-offs and make sure that the Factory Method pattern is a good fit for your specific problem. Overusing or misusing design patterns can lead to unnecessary complexity and can make code harder to understand and maintain.
What Can You Do Next 🙏😊
If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for ssoftware developers.