How to implement Abstract Factory design pattern using TypeScript?
In this post, I’ll be explain how to implement Abstract Factory design pattern using TypeScript. I’ll cover some other popular design patterns soon. This post is part of the Design Patterns in TypeScript series. If you would like to learn more, please subscribe to my website for new updates.
What is Abstract Factory pattern?
The abstract factory pattern provides a way to encapsulate a group of individual factories that have a common theme without specifying their concrete classes. In normal usage, the client software creates a concrete implementation of the abstract factory and then uses the generic interface of the factory to create the concrete objects that are part of the theme. The client doesn’t know (or care) which concrete objects it gets from each of these internal factories, since it uses only the generic interfaces of their products. This pattern separates the details of implementation of a set of objects from their general usage and relies on object composition, as object creation is implemented in methods exposed in the factory interface.
– Wikipedia
In other words, an abstract factory provides an interface for creating families of related objects without specifying their concrete classes. If you have a good understanding of Factory method design pattern, it will be really easily to understand this pattern.
With factory method design pattern, a factory is responsible to create objects which have a common interface depending on a certain type. This allows to easily create the objects at run-time based on requirements.
Similarly, there can be times when we need to have different factories that have a common theme/interface, so that depending on some requirement, a concrete factory class can be used via the generic interface of the factory. As such the generic interface is used, the client that request a factory doesn’t need to care which concrete objects it gets from internal factories.
Example
Let us take an example to understand this: we will take two popular brands that manufacture cars – Ford and Renault. Due to the high demand of compact and convertible cars in the market, these two brands need to create cars of such types.
Now to create a car for any given type, we need to have common definition of what a car type is and also an interface of a car factory that will be implemented by these two car makers as shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
namespace AbstractFactoryPattern { export interface ICar{ numberOfDoors: Number; start(): Boolean; } export interface ICarFactory{ make: String; createCar(carType: CarType): ICar; } export enum CarType { Compact = 0, Convertible = 1 } } |
Next, both of the car makers needs to have their own implementation of the car types by using the interface ICar
as shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
namespace AbstractFactoryPattern { class FordCompactCar implements ICar { numberOfDoors: Number = 4; start(): Boolean { return true; } } class FordConvertibleCar implements ICar { numberOfDoors: Number = 2; start(): Boolean { return true; } } class RenaultCompactCar implements ICar { numberOfDoors: Number = 4; start(): Boolean { return true; } } class RenaultConvertibleCar implements ICar { numberOfDoors: Number = 2; start(): Boolean { return true; } } } |
Now let us setup two factory classes one for each make so that each factory is able to create a car based on requested car type. If a consumer/client needs a car, it just needs to know the make and car type and then it can request a car factory to do this job.
Next, let’s define two car factory classes for these two makers that uses the interface ICarFactory
and gives car back depending on the car type.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
namespace AbstractFactoryPattern { export class FordCarFactory implements ICarFactory { make: String = 'FORD'; createCar(carType: CarType): ICar { let car: ICar = null; switch (carType) { case CarType.Compact: car = new FordCompactCar(); break; case CarType.Convertible: car = new FordConvertibleCar(); break; } return car; } } export class RenaultCarFactory implements ICarFactory { make: String = 'RENAULT'; createCar(carType: CarType): ICar { let car: ICar = null; switch (carType) { case CarType.Compact: car = new RenaultCompactCar(); break; case CarType.Convertible: car = new RenaultConvertibleCar(); break; } return car; } } } |
As you can see, using the interfaces, we can add a new brand or a new car anytime. All we will need to do is use both of the interfaces to create a new factory per new make and create a new car.
Let us now consume this as a client below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
/// <reference path="abstractFactory.ts" /> namespace AbstractFactoryPattern { class CarFactoryProducer { static getCarFactory(make: String): ICarFactory { let carFactory: ICarFactory; switch (make) { case "ford": carFactory = new FordCarFactory(); break; case "renault": carFactory = new RenaultCarFactory(); break; } return carFactory; } } export namespace Demo { export function show() { var make = "ford"; let carFactory: ICarFactory = CarFactoryProducer.getCarFactory(make); let car: ICar = carFactory.createCar(CarType.Compact); console.log('The make of car is ' + carFactory.make + ' and it has ' + car.numberOfDoors + ' doors.'); } } } |
The car factory is accessed using generic interface named ICarFactory
that doesn’t care about the concrete type of car factory class is passed at run-time. The code above uses a CarFactoryProducer
class and consumes its method named getCarFactory
with a make to use relevant car factory depending on the passed make value.
Then the car factory is used to create a car, notice the usage of interface ICar
for car variable above. Even this one doesn’t care about the concrete type of car. It is up to you, the author of the code to decide whether you would like to throw an error/exception (not supported type) when a matching make or car type is not provided.
Finally, we can see the output of this demo below:
1 |
The make of car is FORD and it has 4 doors. |
I hope this post properly explains how to implement Abstract Factory design pattern using TypeScript. Please refer Design Patterns in TypeScript series to learn more about design patterns in TypeScript.