Imagine you are playing a game like Minecraft where the open world is generated as you move forward. As you explore, you may come across a variety of different plants and animals. However, certain types of plants and animals will only appear in specific areas. For example, cactus plants can only be found in the desert, bamboo grows exclusively in the jungle, and sweet berries can only be found in the taiga. Similarly, certain animals, such as rabbits, pandas, and wolves, will only inhabit certain biomes.

We need a solution for creating animals and plants for specific biomes. We also need a structured approach for handling interaction between a player and animals.

Player’s interaction with an animal

In our game, we have two classes: Player and Animal. The player can try to hunt an animal by striking it. Somewhere in our code the player object will call the handleStrike method on a specific animal instance. We want to make sure that a player instance can do that without knowing if a specific instance of an animal is a wolf, a rabbit or a panda.

Implementation

We can achieve it by first introducing an interface for an animal with a single function that handles players’ strikes. I will use typescript interface although you can achieve the same thing by using abstract class.

// Abstract Product
interface Animal {
  handleStrike: () => void
}

Now, let’s introduce two animals that have different reactions when they are struck.

// Concrete Product
class Wolf implements Animal {
  handleStrike() {
    // attack the player
  }
}

// Concrete Product
class Rabbit implements Animal {
  handleStrike() {
    // run away
  }
}

At this point, we have simply utilized polymorphism. Let’s create some factory classes that will take care of creating concrete instances of animals for certain biomes. We need to ensure that all factory classes can be called in the same way, therefore first we will create an abstract factory. Once again we will use typescript interface instead of an abstract class.

// Abstract Factory
interface BiomeFactory {
  createAnimal: () => Animal
}

For the sake of simplicity, we are limiting the family of objects to animals. However, you can imagine that the BiomeFactory could also be responsible for creating plants, buildings, landscapes, and so on.

Let’s create concrete factories for our biomes – taiga and desert.

// Concrete Factory
class TaigaFactory implements BiomeFactory {
  createAnimal () {
    return new Wolf()
  }
}

// Concrete Factory
class DesertFactory implements BiomeFactory {
  createAnimal () {
    return new Rabbit()
  }
}

Before we can use our factory pattern, we need to create a Player class. In this example, the only way to interact with an animal is to strike it, but you can imagine that the player would also like to be able to tame, feed, or lure animals

class Player {
  strike(animal: Animal) {
    animal.onStrike()
  }
}

The following function is not a part of the factory pattern, but for the sake of the example, let’s imagine that we need to get biome factories based on the type of biome that the player is currently in.

const getBiomeFactory = (biomeType: "taiga" | "desert"): BiomeFactory => {
  switch (biomeType) {
    case 'taiga':
      return new TaigaFactory()

    case 'desert':
      return new DesertFactory()

    default:
      throw new Error('wrong_biome_type')
  }
}

Now we are ready to insatiate the player, the biome factory and an animal. We only need to know what type of biome we are working with. The BiomeFactory and getBiomeFactory will take care of the reset.

Our player doesn’t need to know what kind of biome he is dealing with or what kind of animal he is interacting with. All he knows is that he can strike.

const player = new Player();
const biomeFactory = getBiomeFactory('taiga')
const animal = biomeFactory.createAnimal()

player.strike(animal)

Concrete factory needs only one instance. While using strictly OOP languages you would create this instance during runtime. It is a common approach to instantiate factories using the singleton pattern. In JavaScript we can take advantage of object literals. This means we can actually create concrete factory objects without having concrete factory class at all.

const MyConcreteFactory: MyAbstractFactory = {
  createConcreteProduct: () => new ConcreteProduct()
}

Elements of factory pattern

Let’s review all the parties of the factory pattern and how they relate to our example.

  • Abstract Factory BiomeFactory – defines a set of methods for creating abstract products objects
  • Concrete Factory TaigaFactory – implements the set of methods for creating concrete product objects
  • Abstract Product Animal – defines an interface for a type of product objects
  • Concrete Product Wolf– provides a concrete implementation of the Abstract Product and specifies the specific type of product that will be created by the corresponding concrete factory
  • Client Player – uses only interfaces provided by Abstract Factory and Abstract Product

Summary

Let’s take a quick look on pros and cons of using factory pattern.

Pros

  • Isolates concrete classes. It enhances control over the classes of objects. It isolates the client ( player ) and process of product ( animal ) creation. Client manipulates instances through their abstract interfaces. Product class names are isolated in the implementation of the concrete factory, they don’t appear in client code. Therefore in our example player can just strike an animal, at this point we don’t know if the animal is a wolf or a rabbit.
  • Makes exchanging product families easy. This means that we can easily switch concrete products or entire families. Simply by switching from TigaFactory to DesertFactory we will get an entire new family of objects.
  • Promotes consistency Since creation of a concrete product happens only in one place in the code, we can be sure that the product will remain consistent through the entire application. You can easily replace or change the definition of concrete products. Imagine how easy it would be to change a wolf’s behavior or how easy it would be to change taiga’s animal from wolf to tiger.

Cons

  • supporting new kinds of products is difficult – Adding a new product to an abstract factory will mean we need to extend every concrete factory with this new kind of product. If you were to add a product that only exists in desert biome this would become uneasy to apply with factory pattern

Links