Software Design Pattern: the Factory

Simplify construction of your like classes with the factory.

The Factory Pattern!

The factory pattern is a way to replace the normal construction of an object, allowing your application to use the factory to handle the construction, in essence, decoupling the construction of the object, from the use of the object. A well-designed factory also makes the application more robust and flexible to future changes.

The factory pattern can be thought of as a car factory. Think about the complexity that goes into fine tuning an engine and then mounting it inside of the body of a car. Do you need to know how to build a car in order to drive or use a car? Of course not! The factory pattern is that same idea. It allows you to not have to worry about the building of objects and in turn focus on the use of that object.

Why the factory pattern?

Advantages

Think about our car factory example. Suppose our cars all have drive() as a method because they implement a Vehicle interface. If our car factory also started producing motorcycles, it would be easy for us as the consumer to also know how to drive the motorcycle. We wouldn't need to learn how to make or construct motorcycles, but could instead simply call the same drive() method, and off we would go! So the factory allows us the flexibility in the usage and implementation of other subclasses or components that use the factory.

The factory allows you to use abstraction to simplify your logic not only around creation, but also around usage of any interface or abstract class. This leads to more extensibility to business changes and quicker turnaround in maintenance.

Disadvantages

The factory pattern is not the one size fits all when you are hoping to abstract. A jump too early to try to simplify construction of your classes can lead to added complexity to what could have been simple codebases. The factory does add extra complexity, so think through if the advantage is there.

The factory can also pigeonhole the code creation to a single factory, which could lead to added complexity or friction when the client code needs even more added flexibility, specifically around creation.

TypeScript Implementation

We want our client code to be able to handle many different types of employees, and also give us the room to add new employee types in the future. So, we are going to use a factory to make things a little straighter forward for us!

First, we will set up an EmployeeType enum, NewHire interface, and an abstract Employee class.

enum EmployeeType { 
    SoftwareDeveloper = 'SoftwareDeveloper',
    DataScientist = 'DataScientist',
    Intern = 'Intern',
    Manager = 'Manager'
}

interface NewHire {
    name: string,
    job: EmployeeType,
    salary: number
}

abstract class Employee {
    public name: string;
    public pmtPerYear: number;
    public  pmtPerPaycheck: number;

    assignAnnualPay(annualPay: number): void {
        this.pmtPerPaycheck = annualPay / this.pmtPerYear;
    }

    setName(name: string): void {
        this.name = name;
    }

    pay(): void {};
}

Now we can add some different employee types that will extend from our Employee class.

export class SoftwareDeveloper extends Employee {
    constructor() {
        super()
        this.pmtPerYear = 52;
    }

    public pay(): void {
        console.log(`Paying out $${this.pmtPerPaycheck} to Software Developer`);
    }
}

export class DataScientist extends Employee {
    constructor() {
        super()
        this.pmtPerYear = 24;
    }

    public pay(): void {
        console.log(`Paying out $${this.pmtPerPaycheck} to Data Scientist`);
    }
}

export class Intern extends Employee {
    constructor() {
        super()
        this.pmtPerYear = 26;
    }

    public pay(): void {
        console.log(`Paying out $${this.pmtPerPaycheck} to Intern`);
    }
}

export class Manager extends Employee {
    public employees: Employee[];

    constructor() {
        super()
        this.pmtPerYear = 52;
    }

    public pay(): void {
        console.log(`Paying out $${this.pmtPerPaycheck} to Software Developer`);
    }

    public getEmployees(): void {
        console.log(`${this.name} manages the following employees: ${this.employees.map(employee => employee.name).join(',')}`)
    }
}

Now let's add in our factory that will handle the creation of our different classes.

export class EmployeeFactory {
    public static createEmployee(type: EmployeeType): Employee {
        if (type === EmployeeType.SoftwareDeveloper) {
            return new SoftwareDeveloper();
        } 
        else if (type === EmployeeType.DataScientist) {
            return new DataScientist();
        } 
        else if (type === EmployeeType.Intern) {
            return new Intern();
        } 
        else if (type === EmployeeType.Manager) {
            return new Manager();
        }
        else {
            throw new Error(`Employee type of ${type} was unexpected.`)
        }
    }
}

Let's now add our hireEmployees function which will take our list of employees, create them with our EmployeeFactory and set their name and annual pay. From here, we can go ahead and run our demo and see how it turned out!

function hireEmployees(newHires: NewHire[]): Employee[] {
    let employees: Employee[] = [];

    newHires.forEach((newHire) => {
        let employee = EmployeeFactory.createEmployee(newHire.job);
        employee.setName(newHire.name);
        employee.assignAnnualPay(newHire.salary);

        employees.push(employee);
    })

    return employees;
}

function runFactoryDemo() : void {
    let newHires: NewHire[] = [
        { name: 'Michael', job: EmployeeType.Manager, salary: 150000 },
        { name: 'Pam', job: EmployeeType.SoftwareDeveloper, salary: 120000 },
        { name: 'Jim', job: EmployeeType.SoftwareDeveloper, salary: 100000 },
        { name: 'Kelly', job: EmployeeType.DataScientist, salary: 140000 },
        { name: 'Ryan', job: EmployeeType.Intern, salary: 40000 },
    ];

    let employees: Employee[] = hireEmployees(newHires)

    console.log('*** Showing Current Employees ***')
    employees.forEach((employee) => {
        console.log(employee)
        console.log()
    });
}

runFactoryDemo();

👏Success!👏

Everything worked as planned! We were able to make the hiring of our employees simple and dynamically use the methods of those different employee classes. We didn't need to know what type of employee they were, but by using the factory, we were able to abstract that logic out!

Check out the full demo here: the Factory. To try it out, simply run the typescript compiler tsc then run node factory.js from the console.

Conclusion

The factory is a great tool in any software developer's toolkit. It gives our code some much needed flexibility and extensibility to handle change. If you are handling the construction of many different, similar, classes, then think about if the factory would work for you.


Want to be notified about new posts?

Then sign up to get posts directly to your email. You can opt-out at any time.

Drake Loud - © 2024