Arkar Myat

A guide to Abstract Factory Design Pattern

Aug 15, 2023Arkar
Design Patterns

Discover how the Abstract Factory pattern simplifies object creation and makes it easier to modify later.

On this page

What is Abstract Factory Design Pattern?

In this article, we gonna talk about the Abstract Factory design pattern and how it can be used effectively in our applications.

Let's say we're making a game with different characters, like warriors, mages, and archers. Each type of character has its own special abilities, like strength, intelligence, and agility. We want to make these characters in a way that's flexible and easy to keep up with.

With the Abstract Factory, it gives you a way to make objects from each class of the product family. If your code uses this way to make objects, you do not need to worry about making a wrong version of a product that does not match the other products made by your app.

article image

Let’s take a look at our RPG game example, we first define CharacterFactory interface with methods for creating our characters.

interface Character {
  strength: number;
  intelligence: number;
  agility: number;
}
interface CharacterFactory {
  createWarrior(): Character;
  createMage(): Character;
  createArcher(): Character;
}

To simplify the implementation of the Character class, we can create classes for each type of character, such as Warrior, Mage, and Archer, that implement the Character interface. We can also add their own unique methods that associates with their nature.

We can create different types of characters, such as warriors, mages, and archers. Each type of character has unique abilities and equipments. For example, warriors have weapons to fight with, mages have spells to cast, and archers have bows to shoot arrows with.

interface Character {
  strength: number;
  intelligence: number;
  agility: number;
}

abstract class CharacterClass implements Character {
  constructor(
    public strength: number,
    public intelligence: number,
    public agility: number
  ) {}
}

class Warrior extends CharacterClass {
  weapon: string;

  constructor(
    strength: number,
    intelligence: number,
    agility: number,
    weapon: string
  ) {
    super(strength, intelligence, agility);
    this.weapon = weapon;
  }

  fight() {
    console.log(`The warrior attacks with ${this.weapon}!`);
  }
}

class Mage extends CharacterClass {
  spell: string;

  constructor(
    strength: number,
    intelligence: number,
    agility: number,
    spell: string
  ) {
    super(strength, intelligence, agility);
    this.spell = spell;
  }

  castSpell() {
    console.log(`The mage casts ${this.spell}!`);
  }
}

class Archer extends CharacterClass {
  bow: string;

  constructor(
    strength: number,
    intelligence: number,
    agility: number,
    bow: string
  ) {
    super(strength, intelligence, agility);
    this.bow = bow;
  }

  shootArrow() {
    console.log(`The archer shoots an arrow with ${this.bow}!`);
  }
}

To implement the CharacterFactory with the Warrior class, we would create a concrete WarriorFactory class that implements the CharacterFactory interface. The WarriorFactory class would then implement the createWarrior() method, which would create a new instance of the Warrior class with the appropriate values for strength, intelligence, agility, and weapon.

class WarriorFactory implements CharacterFactory {
  createWarrior() {
    return new Warrior(10, 5, 7, 'Sword');
  }

  createMage() {
    return new Mage(5, 10, 7, 'Fireball');
  }

  createArcher() {
    return new Archer(7, 5, 10, 'Bow');
  }
}

As shown in the example above, we can also create concrete factory classes for the Mage and Archer classes, which would implement the createMage() and createArcher() methods, respectively. By using the Abstract Factory pattern, we can easily create different types of characters and ensure that they are consistent and match the other products made by our app.

The CharacterFactory interface helps create different kinds of characters. Each method in the interface is implemented by different factory classes, such as WarriorFactory, MageFactory, and ArcherFactory. These factories let us create different types of characters that fit with the other products in our app.

However, the pattern can be taken a step further by creating factories of factories. For example, let's say our game has monsters with the same characteristics as warriors. By creating a factory of factories, we can simplify the implementation even further and ensure consistent products across the board.

Here's an example of a MonsterFactory that also implements the CharacterFactory interface:

class MonsterFactory implements CharacterFactory {
  createWarrior() {
    return new Monster(10, 5, 7, 'Claws');
  }

  createMage() {
    return new Monster(5, 10, 7, 'Spit');
  }

  createArcher() {
    return new Monster(7, 5, 10, 'Acid');
  }
}

class Monster extends CharacterClass {
  attack: string;

  constructor(
    strength: number,
    intelligence: number,
    agility: number,
    attack: string
  ) {
    super(strength, intelligence, agility);
    this.attack = attack;
  }

  useAttack() {
    console.log(`The monster attacks with ${this.attack}!`);
  }
}

Now, our client code can use the factory consistently.

function createCharacter(factory: CharacterFactory) {
  const warrior = factory.createWarrior();
  const mage = factory.createMage();
  const archer = factory.createArcher();
	console.log(warrior, mage, archer);
}

const warriorFactory = new WarriorFactory();
createCharacter(warriorFactory);

const monsterFactory = new MonsterFactory();
createCharacter(monsterFactory);

So now we can see how abstract factory allows for the creation of objects from each class of a product family, ensuring that they are consistent and match other products made by the application.

The Abstract Factory design pattern simplifies creating groups of objects that work together without specifying which objects they are. It provides a centralized location for these objects, making it easier to modify later. Additionally, this pattern allows for easy addition of new object groups without altering the existing code, thus following the Open/Closed principle.

However, the Abstract Factory pattern can increase code complexity, especially when dealing with many product families. This requires creating many related classes, which can be difficult to manage and time-consuming. For smaller applications with fewer product families, this pattern may not be necessary.

Thank you for you time and I hope this article helps ❤️❤️❤️.

Complete Code

interface Character {
  strength: number;
  intelligence: number;
  agility: number;
}

abstract class CharacterClass implements Character {
  constructor(
    public strength: number,
    public intelligence: number,
    public agility: number
  ) {}
}

class Warrior extends CharacterClass {
  weapon: string;

  constructor(
    strength: number,
    intelligence: number,
    agility: number,
    weapon: string
  ) {
    super(strength, intelligence, agility);
    this.weapon = weapon;
  }

  fight() {
    console.log(`The warrior attacks with ${this.weapon}!`);
  }
}

class Mage extends CharacterClass {
  spell: string;

  constructor(
    strength: number,
    intelligence: number,
    agility: number,
    spell: string
  ) {
    super(strength, intelligence, agility);
    this.spell = spell;
  }

  castSpell() {
    console.log(`The mage casts ${this.spell}!`);
  }
}

class Archer extends CharacterClass {
  bow: string;

  constructor(
    strength: number,
    intelligence: number,
    agility: number,
    bow: string
  ) {
    super(strength, intelligence, agility);
    this.bow = bow;
  }

  shootArrow() {
    console.log(`The archer shoots an arrow with ${this.bow}!`);
  }
}
interface CharacterFactory {
  createWarrior(): Character;
  createMage(): Character;
  createArcher(): Character;
}

class WarriorFactory implements CharacterFactory {
  createWarrior(): Character {
    return new Warrior(10, 2, 5, "Knife");
  }
  createMage(): Character {
    return new Mage(2, 10, 5, "Fireball");
  }
  createArcher(): Character {
    return new Archer(5, 2, 10, "Longbow");
  }
}

class MonsterFactory implements CharacterFactory {
  createWarrior() {
    return new Monster(10, 5, 7, "Claws");
  }

  createMage() {
    return new Monster(5, 10, 7, "Spit");
  }

  createArcher() {
    return new Monster(7, 5, 10, "Acid");
  }
}

class Monster extends CharacterClass {
  attack: string;

  constructor(
    strength: number,
    intelligence: number,
    agility: number,
    attack: string
  ) {
    super(strength, intelligence, agility);
    this.attack = attack;
  }

  useAttack() {
    console.log(`The monster attacks with ${this.attack}!`);
  }
}

function createCharacter(factory: CharacterFactory) {
  const warrior = factory.createWarrior();
  const mage = factory.createMage();
  const archer = factory.createArcher();
  console.log(warrior, mage, archer);
}

const warriorFactory = new WarriorFactory();
createCharacter(warriorFactory);

const monsterFactory = new MonsterFactory();
createCharacter(monsterFactory);
Subscribe to my NewsLetter!

Join my web development newsletter to receive the latest updates, tips, and trends directly in your inbox.