The Power of Factory Design Pattern in JavaScript
jsmanifest
Posted on March 19, 2022
As programmers we are always trying to make good decisions when we write code. It's not always an easy task especially when our code becomes larger over time. Fortunately there are proven ways to pick one implementation over another when the right opportunity arrives.
If you're new to programming, you might have not come across a situation yet where you have a complex object and needed to employ the Factory pattern to abstract away the complexities. If you plan on continuing your future with writing code, then this post will help you.
In this post we will be going over the Power of Factory Design Pattern in JavaScript, which is one way to break a complex object apart into simpler objects to avoid unnecessary complexity. Keep in mind that we're going to follow the DRY principle as a best practice.
When we think of a factory in the real world we think of some laboratory that makes stuff. That is exactly what the factory pattern is when we translate it to code.
Let's pretend we are building an MMORPG game where we will go over the parts that take advantage of this pattern and we will see how it benefits our applications.
We will have a Game
class, a Profile
to create profiles when users open our software, and the four classes that profiles will create as characters for our users to choose:
class Mag extends Character {}
class Thief extends Character {}
class Archer extends Character {}
class Warrior extends Character {}
class Profile {
constructor(name, email = '') {
this.name = name
this.email = email
}
createCharacter(classType) {
switch (classType) {
case 'archer':
this.character = new Archer()
return this.character
case 'mage':
this.character = new Mage()
return this.character
case 'thief':
this.character = new Thief()
return this.character
case 'warrior':
this.character = new Warrior()
return this.character
default:
throw new Error(
`Invalid class type "${classType}". Choose one of: "archer", "mage", "thief", or "warrior"`,
)
}
}
synchronizeProfileContacts(anotherProfile) {
// Do something to inherit anotherProfile's contacts
}
setName(name) {
this.name = name
}
setEmail(email) {
this.email = email
}
}
class Game {
constructor() {
this.users = {}
}
createUser(name) {
const user = new Profile(name)
this.users[user.id] = user
return user
}
}
const game = new Game()
const bobsProfile = game.createUser('bob')
const bobsMage = bobsProfile.create('mage')
Three months later we decide we want to implement another character class called Shaman
.
In order to do that we have to create the class:
class Shaman extends Character {}
When we want to allow users to select the Shaman
class after the update and call profile.createCharacter
we'll get this error:
Error: Invalid class type "shaman". Choose one of: "archer", "mage", "thief", or "warrior"
That's because we have to change the create
method on the Profile
class.
After we change it to this, it will work:
class Profile {
constructor(name, email = '') {
this.name = name
this.email = email
}
createCharacter(classType) {
switch (classType) {
case 'archer':
this.character = new Archer()
return this.character
case 'mage':
this.character = new Mage()
return this.character
case 'shaman':
this.character = new Shaman()
return this.character
case 'thief':
this.character = new Thief()
return this.character
case 'warrior':
this.character = new Warrior()
return this.character
default:
throw new Error(
`Invalid class type "${classType}". Choose one of: "archer", "mage", "shaman", "thief", or "warrior"`,
)
}
}
synchronizeProfileContacts(anotherProfile) {
// Do something to inherit anotherProfile's contacts
}
setName(name) {
this.name = name
}
setEmail(email) {
this.email = email
}
}
This is the problem the factory design pattern solves.
What if we wanted to add 3 more character classes? We have to change the implementation 1-3 times.
Remember when we mentioned we were going to follow the DRY principle, as every developer should? This violates that rule!
If you're new to programming this doesn't sound like a big deal judging only from the code we currently have. That's because our Game
class only has a createUser
method, but in the real world MMORPG games definitely grow much more in code size due to all of the necessary features that make it more valuable for entertainment for their users.
Our Game
class will likely have tons of different methods needed to implement plenty of features, such as createTerrain
, createEquipment
, createMonster
, createAttack
, createPotion
, createRaid
, createBuilding
, createShop
, etc.
Unfortunately every one of those methods most likely need to be extended further because they will each need to create different types. For example the createEquipment
might need to implement a way to create sword equipment, staffs, boots, armors, which all most likely need to produce further variants of types like the type of sword and boots.
So if we wanted to implement all of those right now we have to go change every method exactly like we did when we first wrote our Shaman
class, and we already suffered from our first error because we forgot to add in Shaman in the implementation of our Profile.createUser
method.
If we stopped with the factories here then three months later this will quickly become overwhelming because we're forced to jump to every method and change them.
This is where the factory pattern shines as the code grows larger.
What if Profile.createCharacter
could just stay unchanged so we don't have to touch it ever again? It doesn't need to know which type or kind of character class it creates. It just needs to be given a character class and store it in its instance.
If we wanted to add 10 more character classes we have to manually hunt the same function down and update it even though the Profile
doesn't care about what type of character classes is being produced because it only cares about methods like setName
and synchronizeProfileContacts
.
We can abstract out that part and put it into a factory to produce those objects instead:
class CharacterClassCreator {
create(classType) {
switch (classType) {
case 'archer':
return new Archer()
case 'mage':
return new Mage()
case 'shaman':
return new Shaman()
case 'thief':
return new Thief()
case 'warrior':
return new Warrior()
default:
throw new Error(
`Invalid class type "${classType}". Choose one of: "archer", "mage", "thief", or "warrior"`,
)
}
}
}
Our Profile
class can look more elegantly to accommodate this change:
class Profile {
constructor(name, email = '') {
this.name = name
this.email = email
}
synchronizeProfileContacts(anotherProfile) {
// Do something to inherit anotherProfile's contacts
}
setName(name) {
this.name = name
}
setEmail(email) {
this.email = email
}
setCharacter(character) {
this.character = character
}
}
We aren't violating the DRY principle anymore. Hurray! We only need to change CharacterClassCreator
if we wanted to implement more character classes to create. It's the single responsibility we set it to do--to produce different character class objects.
Here is a visual of what we originally had prior to the factory:
And this is what the Profile
looks like now:
Great! We kept the profile looking nice and clean. We enabled our Profile
class to only focus on its logic.
If you're wondering where the CharacterClassCreator
stands in this, this is actually what is happening behind the scenes:
We added a middle man (the factory) to handle the logic for creating character classes instead. From now on whenever we need to update the implementation to that code we only need to change the CharacterCreationClass
.
I hope you can start noticing the benefit by this stage. Remember when we talked about other methods our Game
class will eventually have such as createBuilding
and createTerrain
? If we apply a similar factory approach to all of them it will be the same process. This allows each of those classes to focus on their own logic.
Let's continue further with our code.
In MMORPG games, different character classes wear different equipment.
For example, magicians usually use staffs, warriors wear heavy steel armor and carry swords, thieves carry one or two daggers, and archers use crossbows.
In addition there are usually some perks if users register an account and buy some type of membership to come with it.
Here is what that might look like:
class Equipment {
constructor(name) {
this.name = name
}
}
class CharacterClassCreator {
async applyMembershipCode(code) {
// return fetch(`https://www.mymmorpg.com/api/v1/membership-code?code=${code}`)
// .then((resp) => resp.json())
// .then((data) => data)
return { equipments: [{ type: 'ore' }] }
}
async create(profile, classType) {
const creatorMap = {
archer: {
Class: Archer,
},
mage: {
Class: Mage,
},
shaman: {
Class: Shaman,
},
thief: {
Class: Thief,
},
warrior: {
Class: Warrior,
},
}
let character
// Each character class has a different starter weapon
let starterWeapon
if (creatorMap[classType]) {
const { Class, membership } = creatorMap[classType]
character = new Class()
if (character instanceof Archer) {
starterWeapon = new Equipment('crossbow')
} else if (character instanceof Mage) {
starterWeapon = new Equipment('staff')
} else if (character instanceof Shaman) {
starterWeapon = new Equipment('claw')
} else if (character instanceof Thief) {
starterWeapon = [new Equipment('dagger'), new Equipment('dagger')]
} else if (character instanceof Warrior) {
starterWeapon = new Equipment('sword')
}
character.useEquipment(starterWeapon)
if (typeof profile.code === 'number') {
if (profile.code) {
const { equipments: _equipments_ } = await this.applyMembershipCode(
profile.code,
)
// There are equipments provided in addition to the starter weapon.
// This is most likely the result of the user paying for extra stuff
_equipments_.forEach((equipment) => {
// For thief class that uses duo daggers
if (Array.isArray(equipment)) {
character.useEquipment(equipment[0])
character.useEquipment(equipment[1])
} else {
character.useEquipment(equipment)
}
if (membership) {
if (membership.status === 'gold') {
// They bought a gold membership. Ensure we apply any starter equipment enhancents they bought with their membership at checkout when they created a new account
if (membership.accessories) {
membership.accessories.forEach(({ accessory }) => {
if (accessory.type === 'ore') {
// Each ore has an 80% chance of increasing the strength of some weapon when using some enhancer
const { succeeded, equipment } = this.applyEnhancement(
starterWeapon,
accessory,
)
if (succeeded) starterWeapon = equipment
} else if (accessory.type === 'fun-wear') {
// They also bought something fancy just to feel really cool to their online friends
character.useEquipment(new Equipment(accessory.name))
}
})
}
}
}
})
}
}
} else {
throw new Error(
`Invalid class type "${classType}". Choose one of: "archer", "mage", "shaman", "thief", or "warrior"`,
)
}
return character
}
applyEnhancement(equipment, ore) {
// Roll the dice and apply the enhancement on the equipment if it succeeds, then return back the equipment
// Pretend it succeeded
return { equipment, succeeded: true }
}
}
It looks like our CharacterClassCreator.create
method is becoming a little complex. We went back to violating the DRY principle.
But we didn't have much of a choice because it doesn't make sense to put it in Profile
, and we don't want to have this in Game
because Game
will have plenty of methods over time that need to be in scope of a high level. We also can't just hard code it in the global scope. That will make our program become very error prone. We would be polluting the global scope and further extensions to our code will have to involve the global scope.
It now has to be responsible for creating the character class, ensuring the starter weapon is created and attach it to the character, apply (if any) membership perks the user bought with their membership to go with their new character, checking the type of accessory they bought (let's not think about how many different types of accessories our MMORPG will ideally have in the next couple of years) to ensure that they got exactly what they payed for (in this case running an enhancer function), attaching that enhancement in the starter weapon, replace the starter weapon if it was enhanced, and it even became asynchronous!
What if we published this as a library? Every developer's program is going to break now because we stuck a profile
parameter as the first parameter in our CharacterClassCreator
class along with converting it to be asynchronous.
Having to do all of this just to create a character class is too overwhelming for our CharacterClassCreator
class as shown below:
Well, we can just apply more factories and delegate responsibilities of creating these objects that handle their own logic.
I am going to post the extended code and show a diagram of how the the abstraction looks like when applying a couple of factories to solve some of these issues:
class Character {
useEquipment() {}
}
class Mage extends Character {}
class Shaman extends Character {}
class Thief extends Character {}
class Archer extends Character {}
class Warrior extends Character {}
class Profile {
constructor(name, email = '') {
this.initializer = new ProfileInitializer()
this.id = Math.random().toString(36).substring(2, 9)
this.name = name
this.email = email
}
async initialize() {
await this.initializer.initialize(this)
}
synchronizeProfileContacts(anotherProfile) {
// Do something to inherit anotherProfile's contacts
}
setName(name) {
this.name = name
}
setEmail(email) {
this.email = email
}
setCharacter(character) {
this.character = character
}
setMembership(membership) {
this.membership = membership
}
}
class Equipment {
constructor(name) {
this.name = name
}
}
class CharacterClassCreator {
create(classType) {
const creatorMap = {
archer: {
Class: Archer,
},
mage: {
Class: Mage,
},
shaman: {
Class: Shaman,
},
thief: {
Class: Thief,
},
warrior: {
Class: Warrior,
},
}
let character
if (creatorMap[classType]) {
const { Class } = creatorMap[classType]
character = new Class()
return character
} else {
throw new Error(
`Invalid class type "${classType}". Choose one of: "archer", "mage", "shaman", "thief", or "warrior"`,
)
}
}
}
class Membership {
constructor(type) {
this.type = type
}
async applyMembershipCode(profile, code) {
// return fetch(`https://www.mymmorpg.com/api/v1/membership-code?code=${code}`)
// .then((resp) => resp.json())
// .then((data) => data)
return { equipments: [{ type: 'ore' }] }
}
}
class MembershipFactory {
create(type) {
const membership = new Membership(type)
return membership
}
}
class ProfileInitializer {
constructor() {
this.initializers = {}
}
async initialize(profile) {
for (const [name, initialize] of Object.entries(this.initializers)) {
const initialize = profile.initializers[name]
await initialize(profile.character)
}
return profile.character
}
use(name, callback) {
this.initializers[name] = callback
}
}
class EquipmentEnhancer {
applyEnhancement(equipment, ore) {
// Roll the dice and apply the enhancement on the equipment if it succeeds, then return back the equipment
// Pretend it succeeded
return { equipment, succeeded: true }
}
}
class Game {
constructor() {
this.users = {}
}
createUser(name) {
const user = new Profile(name)
this.users[user.id] = user
return user
}
}
;(async () => {
const characterClassCreator = new CharacterClassCreator()
const profileInitializer = new ProfileInitializer()
const equipmentEnhancer = new EquipmentEnhancer()
const membershipFactory = new MembershipFactory()
const game = new Game()
// Initializes the starter weapon
profileInitializer.use(async (profile) => {
let character = profile.character
let starterWeapon
if (character instanceof Archer) {
starterWeapon = new Equipment('crossbow')
} else if (character instanceof Mage) {
starterWeapon = new Equipment('staff')
} else if (character instanceof Shaman) {
starterWeapon = new Equipment('claw')
} else if (character instanceof Thief) {
starterWeapon = [new Equipment('dagger'), new Equipment('dagger')]
} else if (character instanceof Warrior) {
starterWeapon = new Equipment('sword')
}
character.useEquipment(starterWeapon)
})
// Initializes membership perks
profileInitializer.use(async (profile) => {
const character = profile.character
switch (profile.code) {
case 12512: {
// They bought a gold membership.
// Ensure we apply any starter equipment enhancements they included with their membership when they went through the checkout process for creating new accounts
const goldMembership = membershipFactory.create('gold')
profile.setMembership(goldMembership)
const { equipments: _equipments_ } =
await profile.membership.applyMembershipCode(profile.code)
// There are equipments provided in addition to the starter weapon.
// This is most likely the result of the user paying for extra stuff
_equipments_.forEach((equipment) => {
// For thief class that uses duo daggers
if (Array.isArray(equipment)) {
character.useEquipment(equipment[0])
character.useEquipment(equipment[1])
} else {
character.useEquipment(equipment)
}
if (profile.membership.accessories) {
profile.membership.accessories.forEach(({ accessory }) => {
if (accessory.type === 'ore') {
// Each ore has an 80% chance of increasing the strength of some weapon when using some enhancer
const { succeeded, equipment } =
equipmentEnhancer.applyEnhancement(starterWeapon, accessory)
if (succeeded) starterWeapon = equipment
} else if (accessory.type === 'fun-wear') {
// They also bought something fancy just to feel really cool to their online friends
character.useEquipment(new Equipment(accessory.name))
}
})
}
})
break
}
default:
break
}
})
const bobsProfile = game.createUser('bob')
// bobsProfile.code = 12512
const bobsCharacter = await characterClassCreator.create('shaman')
console.log(game)
console.log(bobsProfile)
console.log(bobsCharacter)
})()
And here a visual of what it looks like:
We can clearly see now that the factory has abstracted out some complexities where it makes more sense.
Each class object has their own responsibility. The main concern we had going through the examples in this post is initializing the profile which is our most sensitive part of our code. We want profile to stay simple and allow the factories to handle the abstractions like what kinds of memberships are applied and how they behave. Profile
only worries about ensuring that the profile has the interface it needs to set all the pieces.
Conclusion
Thank you for reading and look forward for more quality posts coming from me in the future!
Find me on medium
Posted on March 19, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 5, 2024
February 6, 2024