DEV Community

Cover image for Design Patterns: Factory - Getting Started with Typescript
Danilo Silva
Danilo Silva

Posted on

Design Patterns: Factory - Getting Started with Typescript

What is a Design Pattern?

Hi devs. We will start talking about design patterns for software development. In day life as a programmer, we face several refactoring issues, clean code reusability and performance problems to make sure our sofware is scalable.

All these problems were shared by dev community for a long time and the result of all discussions was the creation of several paradigms to build a code.

It was created, for example, the object-oriented programming, the four pillars of OOP, the SOLID principles and the design patterns.

Design patterns are patterns of software development with the aim to make code scalable and improve software quality. The standards guarantee ways to solve problems of clarity and code maintainability.

Remember, the patterns are not code blocks to add to your project. They are ways to coding and because of that, the patterns are usable to several languages. We're going to code with typescript, but you can choose the language you like the most.

Factory

The factory, or factory method, is one of the creational patterns (Factory, Abstract Factory, Builder, Prototype and Singleton), so, it's a pattern relative to object instances. It proposes the coding of a method to instance classes objects.

Using factory method, your code will no longer instance class objects with new. Your code is going to call the factory method and this one will return an instantiated object called product.

What's the advantage of using the Factory method?

First of all, you uncouple instances from your code. So the business rule needed by a product is not explained to the rest of your code.

Second, if you use the factory method, you can add features to all processes involving the instance of objects. Without factory, you need to add a change to all 'new' method called.

You can think about adding the feature in constructor but, in this way, you can code against three SOLID principles at once (S, O and D). Let's check examples:

You are coding a car shop management system and in your code you have the entity Car. This class is fully of business rules about car, including taxes calculation and car registration documents.

So we have to develop a new feature: 'When a new car is instantiated, we must log this event'. You must create a Log class responsible for logging these data in a file or database.

If we instantiate an Log object at Car class constructor, we are coding against the single-responsibility principle because the Car class must be responsible only by cars.

We don't use the Open-closed principle because a class should be open for extension and close for modification. In this case, we are changing the constructor.

And we don't use the dependency inversion principle because the Car class can't be used due to the high coupling to Log class. If you want to use the Car class in other system, you should to code the Log class.

The third advantage is that you can return a previously created object instead of instantiating a new one. In this case, we use another creational pattern called Singleton.


Let's Code

We are developing an Airline software and the first class is Airplane. For a while, we won't code complex methods and numerous attributes. We'll have only prefix, manufacturer and aircraft attributes.

We will have the getters methods and all attributes will be assigned in constructor. Our Airplane class implements the IAirplane interface.

Airplane Class

interface IAirplane {
    prefix: string;
    manufacturer: string;
    aircraft: string;
}

class Airplane implements IAirplane {
    constructor(private _prefix: string,
        private _manufacturer: string,
        private _aircraft: string) {}

    get prefix(): string {
        return this._prefix
    }

    get manufacturer(): string {
        return this._manufacturer;
    }

    get aircraft(): string {
        return this._aircraft;
    }

}
Enter fullscreen mode Exit fullscreen mode

For now on, every time we use an object Airplane we would call the new method:

const embraerE195 = new Airplane('PR-ABC','Embraer','E195');
Enter fullscreen mode Exit fullscreen mode

However, we're going to use the factory pattern. The implementation is the creation of a new class called AirplaneFactory. This class must have the factory method called create which it will instantiate a new Airplane product. So, in our code, the factory class is going to be the only one that we can to instantiate with the new method.

Some samples suggest that factory method and factory class are statics. This is a wrong way to code the factory that prevents it to be extended. We will check about creating abstract and concrete products.

Airplane and Factory classes

In factory class and factory method, we code:

class AirplaneFactory {
    public create (prefix: string, manufacturer: string, aircraft: string): Airplane {
        return new Airplane(prefix, manufacturer, aircraft);
    } 
};

Enter fullscreen mode Exit fullscreen mode

and to create a new Airplane:

const airplaneFactory = new AirplaneFactory();

const embraerE195 = airplaneFactory.create('PR-ABC','Embraer','E195');
Enter fullscreen mode Exit fullscreen mode

Note that the factory method must always have a product of the same type as the business rule class, even if it is an abstract product.


Abstracts and concrete factories

We will increase the complexity. In our business rule, from now on, the airplane is an abstract entity to create passenger airplanes and cargo airplanes.

In the diagram, we have the classes PassengerAirplane and CargoAirplane extending the abstract class Airplane and the interfaces IPassengerAirplane and ICargoAirplane.

Abstract and concrete classes

Now we have the abstract class for Airplane

abstract class Airplane implements IAirplane {
    constructor(private _prefix: string,
        private _manufacturer: string,
        private _aircraft: string) {}

    get prefix(): string {
        return this._prefix
    }

    get manufacturer(): string {
        return this._manufacturer;
    }

    get aircraft(): string {
        return this._aircraft;
    }
}
Enter fullscreen mode Exit fullscreen mode

and the concrete classes

interface IPassengerAirplane extends IAirplane {
    passengerCapacity: number;
    buyTicket(): void;
}


class PassengerAirplane extends Airplane implements IPassengerAirplane {

    constructor(prefix: string, manufacturer: string, aircraft: string, private _passengerCapacity: number) {
        super(prefix, manufacturer, aircraft);
    }

    get prefix(): string {
        return super.prefix
    }

    get manufacturer(): string {
        return super.manufacturer;
    }

    get aircraft(): string {
        return super.aircraft;
    }

    get passengerCapacity(): number {
        return this._passengerCapacity;
    }

    public buyTicket(): void {
        console.log(`New ticket emitted to ${this.manufacturer} ${this.aircraft} - Prefix: ${this.prefix}`);
    }
}

Enter fullscreen mode Exit fullscreen mode
interface ICargoAirplane extends IAirplane {
    payload: number;
    loadCargo(weight: number)
}

class CargoAirplane extends Airplane implements ICargoAirplane {
    constructor(prefix: string, manufacturer: string, aircraft: string, private _payload: number) {
        super(prefix, manufacturer, aircraft);
    }

    get prefix(): string {
        return super.prefix
    }

    get manufacturer(): string {
        return super.manufacturer;
    }

    get aircraft(): string {
        return super.aircraft;
    }

    get payload(): number {
        return this._payload;
    }

    public loadCargo(weight: number){
        console.log(`${weight} loaded to ${this.manufacturer} ${this.aircraft} - Prefix: ${this.prefix}`);
    }
}

Enter fullscreen mode Exit fullscreen mode

Now we should fix the factory method. At this moment, we have a factory method to create an Airplane product. We must keep the concrete products as a Airplane product but extending to a PassengerAirplane or CargoAirplane. Because of this, we must have two concrete factories extending from an abstract factory.

The factory method must be an abstract method in the abstract class. The concrete methods must be implemented according to yours definitions.

Abstract and concrete factories

The concrete factories implement the create method. Even the two possible products are different, both of them implements the IAirplane interface. This is a sample to not create a static factory method.

Let's code

abstract class AirplaneFactory {
    public abstract create (prefix: string, 
       manufacturer: string, 
       aircraft: string, 
       payload: number, 
       passengerCapacity: number): Airplane
};

class PassengerAirplaneFactory extends AirplaneFactory {
    public create (prefix: string, 
       manufacturer: string, 
       aircraft: string, 
       passengerCapacity: number): PassengerAirplane {
        return new PassengerAirplane(prefix,
           manufacturer,
           aircraft,
           passengerCapacity);
    } 
};

class CargoAirplaneFactory extends AirplaneFactory {
    public create (prefix: string,
       manufacturer: string, 
       aircraft: string, 
       payload: number): CargoAirplane {
        return new CargoAirplane(prefix,
           manufacturer,
           aircraft,
           payload);
    }
};

Enter fullscreen mode Exit fullscreen mode
const passengerAirplaneFactory = new 
    PassengerAirplaneFactory();

const cargoAirplaneFactory = new 
    CargoAirplaneFactory();

const E195 = passengerAirplaneFactory
    .create('PR-ABC', 
        'Embraer', 
        'E195', 
         118);

const KC390 = cargoAirplaneFactory
    .create('PR-DEF', 
        'Boeing', 
        'B747', 
         137);

E195.buyTicket();
KC390.loadCargo(100);

Enter fullscreen mode Exit fullscreen mode

We create two objects, one called E195 created with the concrete factory PassengerAirplaneFactory and another one called KC390 created with the concrete factory CargoAirplaneFactory. We called the methods buyTicket and loadCargo in spite of both of them are an Airplane product.

If you think that we can code a concrete factory capable to create both products, the answer is yes.

In this case, we have another pattern, the Abstract Factory that is responsible to create families of products. But remember, an abstract factory class is not an abstract factory.


Tests

We can test our factory method to check if the created products are instances of domain classes. We will use Jest but you can use the testing library of your choice.

First of all, I will test the PassengerAirplaneFactory

let passengerAirplaneFactory;
    beforeEach(() => {
        passengerAirplaneFactory = new 
             PassengerAirplaneFactory();
    });
Enter fullscreen mode Exit fullscreen mode

Then, I will test:

  • If the PassengerAirplaneFactory is an instance of it.
  • If the PassengerAirplaneFactory is an instance of AirplaneFactory.
  • If the PassengerAirplaneFactory returns an Airplane product and a PassengerAirplane product.
  • And if the PassengerAirplaneFactory doesn't return a CargoAirplaneFactory product.

describe('Passenger airplane factory', () => {

    let passengerAirplaneFactory;
    beforeEach(() => {
        passengerAirplaneFactory = new 
            PassengerAirplaneFactory();
    });

    it('is a instance of Airplane factory', () => {             
        expect(passengerAirplaneFactory)
            .toBeInstanceOf(AirplaneFactory);
    });

    it('is a instance of Passenger airplane factory', () => {
        expect(passengerAirplaneFactory)
            .toBeInstanceOf(PassengerAirplaneFactory);
    });
    it('creates a airplane and passenger 
    airplane product', () => {
        const E195 = passengerAirplaneFactory
            .create('PR-ABC', 
                'Embraer', 
                'E195', 
                 118); 
        expect(E195).toBeInstanceOf(Airplane);
        expect(E195).toBeInstanceOf(PassengerAirplane);
    });
    it('does not create a cargo airplane product', () => {
        const E195 = passengerAirplaneFactory
            .create('PR-ABC', 
                'Embraer', 
                'E195', 
                118); 
        expect(E195).not.toBeInstanceOf(CargoAirplane);
    });
});

Enter fullscreen mode Exit fullscreen mode

To test CargoAirplaneFactory, we have conditionals similar to PassengerAirplaneFactory.


describe('Cargo airplane factory', () => {

    let cargoAirplaneFactory;
    beforeEach(() => {
        cargoAirplaneFactory = new CargoAirplaneFactory();
    });

    it('is a instance of Airplane factory', () => {    
        expect(cargoAirplaneFactory)
                .toBeInstanceOf(AirplaneFactory);
    });

    it('is a instance of Cargo airplane factory', () => {            
        expect(cargoAirplaneFactory)
            .toBeInstanceOf(CargoAirplaneFactory);
    });

    it('creates a airplane and cargo airplane product', () => 
    {
        const B747 = cargoAirplaneFactory
            .create('PR-DEF', 'Boeing', 'B747', 137); 
        expect(B747).toBeInstanceOf(Airplane);
        expect(B747).toBeInstanceOf(CargoAirplane);
    });

    it('does not create a passenger airplane product', () => 
    {
        const B747 = cargoAirplaneFactory
              .create('PR-DEF', 'Boeing', 'B747', 137); 
        expect(B747).not.toBeInstanceOf(PassengerAirplane);
    });
});

Enter fullscreen mode Exit fullscreen mode

Now we can check the results of our test.

Factory tests

Finally, this is the Factory pattern. I hope this was useful to help you.

Happy studying!

Danilo Silva

Software developer with experience at clean code, patterns and telecommunication hardware and software development.

Linkedin
Github
E-mail

Top comments (0)