Creational Pattern Series | Abstract Factory

Ali Mohammad
8 min readApr 20, 2024

In this series we will take a look at the creational design patterns:

  1. Singleton
  2. Factory Method
  3. Abstract Factory
  4. Prototype
  5. Builder

❤️ What it is?

With the abstract factory pattern, you can make groups of related objects without having to say what their specific classes are.
The Abstract Factory is a design pattern that gives you a way to make a group of related or dependent objects without having to say what their concrete classes are. The pattern gives an abstraction layer that is in charge of making related objects, and the client code uses this interface to make the objects.The why…

🫡 Let’s put it in an example:

Imagine you’re making a game with enemies like dragons, skeletons, zombies, and so on. Each type (family) has products that go with it (Dragon, Zombie), and those products may have different levels of difficulty.
And we don’t want to change the code that’s already there so that it doesn’t give us a headache every time we change the enemies or the difficulty.

The first thing the Abstract Factory pattern says to do is to declare interfaces explicitly for each different product in the product family (e.g., Zombie, Skeleton or Dragon). Then, you can make sure that all versions of a product use the same interfaces. For example, all Skeleton variants can implement the Skeleton interface, and all Zombie variants can implement the Zombie interface, and so on.

The next phase involves creating the Abstract Factory an interface with a list of creation methods for all products in the product family, like createZombie, createDragon, and createSkeleton. These methods must return abstract product types like Skeleton, Zombie, Dragon, and so on, which are shown by the interfaces we already extracted.
Now, what about the different kinds of products? Based on the AbstractFactory interface, we make a separate factory class for each type of a product family. A class that makes products of a certain kind is called a factory. The RegularEnemyFactory, for example, can only make Skeleton and Zombie objects.

The client code must be able to talk to both factories and products through their abstract interfaces. This lets you change the type of a factory that you pass to the client code as well as the product variant that the client code gets without breaking the client code itself.
Let’s say a client asks a factory to make a Skeleton. The client doesn’t have to know what kind of Skeleton it’s getting from the factory, and it doesn’t matter what kind of factory it is. The client must treat all Skeletons the same way by using the abstract Skeleton interface. This is true whether the Skeleton is made of sand or fire. With this method, the client only knows one thing about the Skeleton: that it implements the attack method in some way. Also, whichever variant of the Skeleton is returned, it’ll always match the type of zombie or dragon table produced by the same factory object.

Talk is cheap, show me the code

// Abstract Product Interfaces
interface Enemy {
attack(): void;
}

interface Dragon extends Enemy {
breatheFire(): void;
}
interface Skeleton extends Enemy {
raiseShield(): void;
}
interface Zombie extends Enemy {
infect(): void;
}
// Abstract Factory Interface
interface EnemyFactory {
createDragon(): Dragon;
createSkeleton(): Skeleton;
createZombie(): Zombie;
}
// Concrete Factory 1
class RegularEnemyFactory implements EnemyFactory {
createDragon(): Dragon {
return new RegularDragon();
}
createSkeleton(): Skeleton {
return new RegularSkeleton();
}
createZombie(): Zombie {
return new RegularZombie();
}
}
// Concrete Factory 2
class BossEnemyFactory implements EnemyFactory {
createDragon(): Dragon {
return new BossDragon();
}
createSkeleton(): Skeleton {
return new BossSkeleton();
}
createZombie(): Zombie {
return new BossZombie();
}
}
// Concrete Product 1
class RegularDragon implements Dragon {
attack() {
console.log("Regular Dragon attacks!");
}
breatheFire() {
console.log("Regular Dragon breathes fire!");
}
}
// Concrete Product 2
class RegularSkeleton implements Skeleton {
attack() {
console.log("Regular Skeleton attacks!");
}
raiseShield() {
console.log("Regular Skeleton raises its shield!");
}
}
// Concrete Product 3
class RegularZombie implements Zombie {
attack() {
console.log("Regular Zombie attacks!");
}
infect() {
console.log("Regular Zombie infects its target!");
}
}
// Concrete Product 4
class BossDragon implements Dragon {
attack() {
console.log("Boss Dragon attacks!");
}
breatheFire() {
console.log("Boss Dragon breathes fire!");
}
}
// Concrete Product 5
class BossSkeleton implements Skeleton {
attack() {
console.log("Boss Skeleton attacks!");
}
raiseShield() {
console.log("Boss Skeleton raises its shield!");
}
}
// Concrete Product 6
class BossZombie implements Zombie {
attack() {
console.log("Boss Zombie attacks!");
}
infect() {
console.log("Boss Zombie infects its target!");
}
}
// Client Code
function createEnemy(factory: EnemyFactory, enemyType: string) {
let enemy: Enemy;
switch (enemyType) {
case "dragon":
enemy = factory.createDragon();
break;
case "skeleton":
enemy = factory.createSkeleton();
break;
case "zombie":
enemy = factory.createZombie();
break;
default:
throw new Error("Invalid enemy type!");
}
return enemy;
}
const regularFactory = new RegularEnemyFactory();
const bossFactory = new BossEnemyFactory();
const regularSkeleton = createEnemy(regularFactory, "skeleton");
regularSkeleton.attack(); // Regular Skeleton attacks!
regularSkeleton.raiseShield(); // Regular Skeleton raises its shield!
const bossZombie = createEnemy(bossFactory, "zombie");
bossZombie.attack(); // Boss Zombie attacks!
bossZombie.infect(); // Boss Zombie infects its target!

Another example:

Let’s say you’re creating a user interface for your application, and you need to create different types of buttons and text fields. You want your UI components to have a consistent look and feel, but you also want to be able to easily switch between different styles depending on the user’s preferences or the platform you’re targeting.

To achieve this, you could use the abstract factory pattern. First, you would define a set of interfaces for the different types of UI components you need to create, such as Button and TextField. These interfaces would define the methods and properties that all implementations of the component should have.

Next, you would define an abstract factory interface that has methods for creating instances of each of the UI components you need. For example, you might have a UIFactory interface with methods like createButton and createTextField. Each method would return an instance of the corresponding component interface.

You would then create concrete implementations of the factory interface for each style of UI that you need. For example, you might have a MaterialUIFactory and an iOSUIFactory, each of which creates components that have a distinct look and feel.

Finally, in your client code (i.e., the code that uses the UI components), you would use the abstract factory interface to create instances of the components you need. You would pass an instance of the appropriate concrete factory to the client code, depending on the platform or user preferences. The client code would then use the abstract component interfaces to work with the components, without needing to know which concrete implementations are being used.

// Button interface
interface Button {
text: string;
onClick: () => void;
}

// TextField interface
interface TextField {
value: string;
onChange: (value: string) => void;
}
// Abstract factory interface
interface UIFactory {
createButton(): Button;
createTextField(): TextField;
}
// Concrete Material UI factory
class MaterialUIFactory implements UIFactory {
createButton() {
return {
text: 'Material Button',
onClick: () => console.log('Material button clicked')
};
}
createTextField() {
return {
value: '',
onChange: (value: string) => console.log(`Material text field changed to ${value}`)
};
}
}
// Concrete iOS UI factory
class IOSUIFactory implements UIFactory {
createButton() {
return {
text: 'iOS Button',
onClick: () => console.log('iOS button clicked')
};
}
createTextField() {
return {
value: '',
onChange: (value: string) => console.log(`iOS text field changed to ${value}`)
};
}
}
// Client code
function createUI(factory: UIFactory) {
const button = factory.createButton();
const textField = factory.createTextField();
return { button, textField };
}
// Example usage
const materialUI = createUI(new MaterialUIFactory());
console.log(materialUI.button.text); // "Material Button"
const iosUI = createUI(new IOSUIFactory());
console.log(iosUI.textField.value); // "" (empty string)

🎨 The Structure

🍔 How it works

The Abstract Factory pattern consists of the following components:

  • Abstract Factory: This is an interface that defines the contract for creating the objects. It declares a set of methods that create the related objects.
  • Concrete Factory: This is a class that implements the Abstract Factory interface. It provides an implementation for creating the related objects.
  • Abstract Product: This is an interface that defines the contract for the products that the factories create.
  • Concrete Product: This is a class that implements the Abstract Product interface. It provides the implementation for the products that the factories create.
  • Client: This is the code that uses the factories to create the objects.

The Abstract Factory interface is used by the client code to make the objects. The Abstract Factory makes objects by telling the Concrete Factory to run the right methods. Objects are made by the Concrete Factory by calling the right methods on the Concrete Product.

🏋🏻 When to use the Abstract Factory:

  1. Use the Abstract Factory when your code needs to work with different families of related products, but you don’t want it to depend on the concrete classes of those products. This could be because you don’t know what those concrete classes are yet, or because you want to make room for future additions.
  2. You need to make groups of related objects without saying what their specific classes are.
  3. You want the client code and the concrete classes to be only loosely linked.
  4. You want it to be easy to switch between different groups of objects while the program is running.

✅ Advantages

The Abstract Factory pattern has several advantages, including:

  • It provides a way to create families of related objects without specifying their concrete classes.
  • It promotes loose coupling between the client code and the concrete classes.
  • It makes it easy to switch between different families of objects at runtime.

❌ Drawbacks

The Abstract Factory pattern has some disadvantages, including:

  • It can result in complex code, especially when there are many families of related objects.
  • It can be difficult to extend the pattern to support new families of objects.

🌪️ Conclusion

The Abstract Factory pattern lets you make related objects without specifying their concrete classes. This encourages loose coupling and makes it easy to switch between different families of objects at runtime.

You can find more explanations and code examples in my repository:

Hope you enjoyed the article ❤️

--

--