DEV Community

jsmanifest
jsmanifest

Posted on

The Power of Factory Design Pattern in JavaScript

Post thumbnail

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')
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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"`,
        )
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

profile-create-without-factory-design-pattern

And this is what the Profile looks like now:

profile-create-with-the-power-of-factory-design-pattern

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:

profile-create-with-character-class-creator-factory

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 }
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

javascript-bad-code-before-applying-factory-pattern

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)
})()
Enter fullscreen mode Exit fullscreen mode

And here a visual of what it looks like:

javascript-complexity-reduced-by-factory-design-pattern-result

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

Top comments (5)

Collapse
 
curmichris profile image
Christian Curmi

Love the Abstract Factory pattern and it's used so much without us knowing. Great read

Collapse
 
stephenodogwu profile image
Stephen Odogwu

Also your code under throw new Error below, you didn't include shaman

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",
)
}
}
}

Collapse
 
adrigzr profile image
Adrián González Rus

DRY - Don't Repeat Yourself
SRP - Single Responsibility Principle

I think you got them mixed up.

Collapse
 
stephenodogwu profile image
Stephen Odogwu

For this line is your code,
const bobsMage = bobsProfile.create('mage')

I thought it should be to this

const bobsMage = bobsProfile.createCharacter('mage')

Collapse
 
ooling profile image
Sam oo Líng

I may didn't understand for now but I feels like this will help a lot in the future.
good article anyway and thanks for sharing!