Design patterns can make your code more flexible, more resilient to change and easier to maintain. In this post you will learn to use the the factory pattern in vanilla ES6 using an object-orientated way of programming.
What is the Factory pattern?
The factory pattern is a creational design pattern, which means it deals with object creation. There are 3 types of factory patterns:
- Simple factory
- Factory method
- Abstract factory.
Let's see what they are!
🔨 Simple factory
Generates an instance without exposing any instantiation logic to the client.
When to use?
To avoid repeating the same code to generate objects, place it into a dedicated factory instead.
Example
In this example we'll create a factory that returns a Monster with private fields:
// Simple factory
class Monster {
constructor(type, level) {
this._type = type;
this._level = level;
}
get type() {
return this._type;
}
get level() {
return this._level;
}
}
const MonsterFactory = {
makeMonster: function (type, level) {
return new Monster(type, level);
},
};
const dragon = MonsterFactory.makeMonster("Dragon", 17);
console.log(dragon.level);
🏠Factory method
Provides a way to delegate instantiation logic to child classes.
When to use?
When the client does not know what exact sub-class it might need.
Example
In the following example we create two players: a Warrior
and a Knight
which both inherit from the Player
class. For each player we'll call the fightMonster()
method, which is described in the Player
class. The actual monster that is created, depends on the implementation of the makeMonster
method of the players themselves. The Warrior
creates a Dragon
monster with a health of 50 and after attacking it drops with 10 points:
class Dragon {
constructor() {
this.health = 50;
}
attack() {
this.health -= 10;
}
}
class Snake {
constructor() {
this.health = 40;
}
attack() {
this.health -= 20;
}
}
class Player {
fightMonster() {
const monster = this.makeMonster();
monster.attack();
return monster;
}
}
class Warrior extends Player {
makeMonster() {
return new Dragon();
}
}
class Knight extends Player {
makeMonster() {
return new Snake();
}
}
const player1 = new Warrior();
console.log(player1.fightMonster());
const player2 = new Knight();
player2.fightMonster();
Abstract factory
Encapsulate a group of individual factories with a common goal. It separates the details of implementation of a set of objects from their general usage.
Imagine that you have a furniture shop with chair's and sofa's. Let's say you want to categorize them in e.g. Victorian and Modern furniture. You don't want to change existing classes as future vendors update their catalogs very often.
When to use?
When your code needs to work with various families of related products, but you don’t want it to depend on the concrete classes of those products—they might be unknown beforehand or you simply want to allow for future extensibility.
Example
In the example below we'll set up a class Application
that takes in a factory. Based on the type of factory, e.g. a Windows factory, a certain type of Button
gets returned. In our case a WinButton
as the factory we provide is the WinFactory
.
class WinFactory {
createButton() {
return new WinButton();
}
}
class MacFactory {
createButton() {
return new MacButton();
}
}
class WinButton {
paint() {
console.log("Rendered a Windows button");
}
}
class MacButton {
paint() {
console.log("Rendered a Mac button");
}
}
class Application {
factory;
button;
constructor(factory) {
this.factory = factory;
}
createUI() {
this.button = factory.createButton();
}
paint() {
this.button.paint();
}
}
let factory;
let OS = "Windows";
if (OS === "Windows") {
factory = new WinFactory();
} else if (OS == "Mac") {
factory = new MacFactory();
}
const app = new Application(factory);
app.createUI();
app.paint(); // Output: Rendered a Windows button
And that's it!
More design patterns are coming, thanks for following this tutorial.
Top comments (1)
Why Players makes monsters, doesn't make sense to me... Player should fight monsters not make them.