Design Patterns: Factory - Getting Started with Typescript
Danilo Silva
Posted on March 15, 2023
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.
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;
}
}
For now on, every time we use an object Airplane
we would call the new
method:
const embraerE195 = new Airplane('PR-ABC','Embraer','E195');
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.
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);
}
};
and to create a new Airplane
:
const airplaneFactory = new AirplaneFactory();
const embraerE195 = airplaneFactory.create('PR-ABC','Embraer','E195');
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
.
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;
}
}
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}`);
}
}
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}`);
}
}
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.
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);
}
};
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);
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();
});
Then, I will test:
- If the
PassengerAirplaneFactory
is an instance of it. - If the
PassengerAirplaneFactory
is an instance ofAirplaneFactory
. - If the
PassengerAirplaneFactory
returns anAirplane
product and aPassengerAirplane
product. - And if the
PassengerAirplaneFactory
doesn't return aCargoAirplaneFactory
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);
});
});
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);
});
});
Now we can check the results of our test.
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.
Posted on March 15, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.