Advanced JavaScript Design Pattern: Part 3

Advanced JavaScript Design Pattern: Part 3

The Factory Pattern

ยท

4 min read

The factory pattern is a powerful and widely-used design pattern in object-oriented programming. It allows for the creation of objects without specifying the exact class of the object that will be created. Instead, a factory class or method is responsible for creating the objects, and the client code can use the factory to create objects of different classes based on certain conditions or input.

The factory pattern is particularly useful in situations where a program needs to work with multiple types of objects that have a common interface, but the specific type of object to be created may not be known until runtime. This allows for greater flexibility and code reusability, as the client code does not need to know the specific class of the objects it is creating. It also allows for the easy addition of new classes without affecting the client code, as the factory can simply create new objects of the new class.

One common example of the factory pattern is in the creation of different types of vehicles. A vehicle factory class could have a method that takes in a string parameter indicating the type of vehicle to create (such as "car" or "truck") and then creates and returns an object of the appropriate class.

interface IVehicle {
    drive(): void;
}

class Car implements IVehicle {
    drive() {
        console.log("Driving a car");
    }
}

class Truck implements IVehicle {
    drive() {
        console.log("Driving a truck");
    }
}

const vehicleLookup: { [key: string]: new () => IVehicle } = {
    car: Car,
    truck: Truck
};

class VehicleFactory {
    static createVehicle(type: string): IVehicle {
        const VehicleType = vehicleLookup[type];
        if (!VehicleType) {
            throw new Error("Invalid vehicle type");
        }
        return new VehicleType();
    }
}

let car = VehicleFactory.createVehicle('car');
let truck = VehicleFactory.createVehicle('truck');

car.drive(); // "Driving a car"
truck.drive(); // "Driving a truck"

Another example of a factory pattern is when creating an object of different classes based on the input file format. For example, a file reader factory class could have a method that takes in a file path and then creates and returns a reader object of the appropriate class based on the file format.

import * as path from 'path';

interface IFileReader {
    read(): string;
}

class TxtFileReader implements IFileReader {
    constructor(private filePath: string) {}
    read() {
        return `Reading content of txt file: ${this.filePath}`;
    }
}

class CsvFileReader implements IFileReader {
    constructor(private filePath: string) {}
    read() {
        return `Reading content of csv file: ${this.filePath}`;
    }
}

const fileReaderLookup: { [key: string]: new (filePath: string) => IFileReader } = {
    '.txt': TxtFileReader,
    '.csv': CsvFileReader
};

class FileReaderFactory {
    static createFileReader(filePath: string): IFileReader {
        const extension = path.extname(filePath);
        const FileReaderType = fileReaderLookup[extension];
        if (!FileReaderType) {
            throw new Error("Invalid file format");
        }
        return new FileReaderType(filePath);
    }
}

It's also possible to use factory pattern with Abstract factory pattern. The abstract factory pattern is a design pattern that allows for the creation of families of related objects without specifying their concrete classes. It is often used in conjunction with the factory pattern to create objects that belong to a certain family of related classes.

interface IShape {
    draw(): void;
}

class Circle implements IShape {
    draw() {
        console.log("Drawing Circle");
    }
}

class Square implements IShape {
    draw() {
        console.log("Drawing Square");
    }
}

abstract class ShapeFactory {
    abstract createShape(): IShape;
}

class CircleFactory extends ShapeFactory {
    createShape() {
        return new Circle();
    }
}

class SquareFactory extends ShapeFactory {
    createShape() {
        return new Square();
    }
}

let circleFactory = new CircleFactory();
let squareFactory = new SquareFactory();

let circle = circleFactory.createShape();
let square = squareFactory.createShape();

circle.draw(); // "Drawing Circle"
square.draw(); // "Drawing Square"

In the above example, an interface IShape is defined, with two implementing classes Circle and Square. Also, there are two factory classes CircleFactory and SquareFactory that extend the abstract factory class ShapeFactory, and they both implement the createShape method in order to return an instance of the appropriate shape.

Instead of using if-else statements or switch statements to determine which class to instantiate in a factory pattern, we considered using a lookup function. A lookup function is a function that takes in a key and returns the appropriate value based on the key. This can be useful when there are many classes that need to be instantiated, as it can make the factory code more readable and maintainable.

Conclusion

In this article, we learned factory pattern. The factory pattern is a powerful and widely-used design pattern in object-oriented programming. It allows for the creation of objects without specifying the exact class of the object that will be created. By using lookup function, it makes the factory code more readable, maintainable and scalable.

Did you find this article valuable?

Support Jay Desai by becoming a sponsor. Any amount is appreciated!

ย