TypeScript Abstract Factory Design Pattern
What is abstract factory design pattern?
The abstract factory design pattern is a creational design pattern. It enhances the factory design pattern by multiplying the factory classes and linking them at a higher level with an abstract factory class.
In essence, it works similarly to the factory design pattern. However, by introducing more intervening interfaces between superclasses and their instances, it further enhances the system’s ability to branch into new areas.
There are a few terminologies in the Factory Design Pattern. These are:
- Client: The client application, class, or method that requests an object from the Abstract Factory class. It’s similar to the concrete creator in the factory design pattern.
- Abstract Factory: A high-level class for all the concrete factory classes.
- Concrete Factory: Sub-factory classes of the Abstract Factory class. They contain all the required properties for creating the concrete product.
- Product interface: It’s the interface that intervenes in instantiating of objects from superclasses. It provides specifications for the concerete factory to generate the final object.
- Concrete Product: The final object returned from the factory.
When should the abstract factory design pattern be used?
This pattern can be used when the requirements of an application exceed the capabilities of the factory design pattern and there is a need for a more complex system. Generally, the main differences between the factory design pattern and the abstract factory pattern lie in the number of group objects they handle. If only one group object is employed, the factory design pattern should be sufficient. However, if there are multiple groups of objects that coexist under the same roof, it would be better to employ the abstract design pattern.
How to implement abstract factory design pattern in TypeScript
Let’s try to apply the abstract factory design pattern to a TypeScript example. Imagine we have a store that holds various categories for different types of products. Upon request, it returns the product from their specific category and informs the client about their model and price.
Our issue here is that we deal with different types of product categories. We could employ the factory design pattern for each type of product; however, this wouldn’t assist us in achieving our objectives because we have two levels between the client and product: the category and product layers.
If we only had the product layer, we could easily employ the factory design pattern. To solve our predicament, instead of using the factory design pattern, we decided to use the abstract factory design pattern. We appoint abstract factories to categories and concrete factories to their product types.
By designing this system and utilizing abstract and concrete factories, we centralize the product creation process within a layered system.
First, let’s start by visualizing our design with a UML class diagram:
Abstract factory desing pattern diagram
As depicted in the diagram above, we employed essential components of the abstract factory design pattern by utilizing interfaces and inheritances.
Abstract factory desing pattern code
For better clarity and understanding, we will use three different modules to write our code. These are:
- Electronic Factory
//product interface
interface IElectronic {
product(): string;
}
//concrete product
class Computer implements IElectronic {
protected price: number;
protected model: string;
constructor() {
this.price = 0;
this.model = "unknown";
}
product(): string {
return `product model: ${this.model}\nproduct price: ${this.price}$`
}
}
//final product A
class Laptop extends Computer {
constructor() {
super();
this.price = 22;
this.model = "Laptop";
}
}
//final product B
class Desktop extends Computer {
constructor() {
super();
this.model = "Desktop";
this.price = 50;
}
}
//Concrete Factory
class ElectronicFactory {
public static getProduct(product: string): IElectronic {
if (product == "Laptop") {
return new Laptop();
} else if (product == "Desktop") {
return new Desktop();
} else {
throw new Error('Undefined Product');
}
}
}
export {
IElectronic,
ElectronicFactory
}
- Beverage Factory
//Product Interface
interface IBeverage {
product(): string
}
//Concrete Product
class Coffee implements IBeverage {
protected price: number;
protected model: string;
constructor() {
this.price = 0;
this.model = "unknown";
}
product(): string {
return `product model: ${this.model}\nproduct price: ${this.price}$`
}
}
//Final Product A
class HotCoffee extends Coffee {
constructor() {
super();
this.price = 5;
this.model = "Hot Coffee";
}
}
//Final Product B
class ColdCoffee extends Coffee {
constructor() {
super();
this.price = 2;
this.model = "Cold Coffee";
}
}
//Concrete Factory
class BeverageFactory {
public static getProduct(product: string): IBeverage {
if (product == "Hot Coffee") {
return new HotCoffee();
} else if (product == "Cold Coffee") {
return new ColdCoffee();
} else {
throw new Error('Undefined Product');
}
}
}
export {
IBeverage,
BeverageFactory
}
- Client
import { IBeverage } from "./beverage_factory";
import { IElectronic } from "./electronic_factory";
import { ElectronicFactory } from "./electronic_factory";
import { BeverageFactory } from "./beverage_factory";
//Abstract Factory
interface ICategory extends IBeverage, IElectronic { }
//Client
class Store {
static getProduct(product: string): ICategory {
if (product == "Desktop" || product == "Laptop") {
return ElectronicFactory.getProduct(product);
} else if (product == "Hot Coffee" || product == "Cold Coffee") {
return BeverageFactory.getProduct(product);
} else {
throw new Error('Invalid Product');
}
}
}
const laptop = Store.getProduct("Laptop");
console.log(laptop.product())
const desktop = Store.getProduct("Desktop");
console.log(desktop.product());
const hotCoffee = Store.getProduct("Hot Coffee");
console.log(hotCoffee.product());
const ColdCoffee = Store.getProduct("Cold Coffee");
console.log(ColdCoffee.product());
/*
product model: Laptop
product price: 22$
product model: Desktop
product price: 50$
product model: Hot Coffee
product price: 5$
product model: Cold Coffee
product price: 2$
*/
Abstract factory design pattern advantages
- Abstract creation: Abstract factory allows the creation of a group of objects without the need for specifying their concrete classes. Thereby improving the flexibility and decoupling of code.
- Consistent creation: it ensures that objects in the group are compatible and consistent with each other.
- Substitution: it allows the substitutions within a group of objects without altering the client code.
Abstract factory design pattern disadvantages
- Complexity: Increased number of factories can lead to increased complexity.
- Expanding: introducing a new group of objects might require implementing new dependencies in the codebase, thereby affecting the integrity of the codebase.