Understanding the Abstract Factory Design Patterns
There are 23 classic design patterns which are described in the original book Design Patterns: Elements of Reusable Object-Oriented Software. These patterns provide solutions to particular problems often repeated in software development.
In this article, I am going to describe how the Abstract-Factory Pattern works and when it should be applied.
--
Abstract Factory: Basic Idea
Wikipedia provides us with the following definition:
The abstract factory pattern provides a way to encapsulate a group of individual factories that have a common theme without specifying their concrete classes — Wikipedia
On the other hand, the definition provided by the original book is the following:
Provide an interface for creating families of related or dependent objects without specifying their concrete classes — Design Patterns: Elements of Reusable Object-Oriented Software
On many occasions, we need to create different types of objects that are not known a priori from a list of possible objects in which these objects are related in the creation process. The natural tendency is to create a factoryManager
class that allows us to obtain the different types of objects based on a parameter. However, this solution has two serious drawbacks that we will describe throughout this article:
-
It breaks the principle of Open-Closed Principle which gives code that is not clean; and that it is not easy to maintain when the software scales.
-
The
factoryManager
class is attached to all types of objects that you want to build, creating code known as spaghetti code.
This problem and its solution have been dealt with in the article in which the Factory-Method design pattern is presented, which allows solving this problem when the creation of objects is simple and they are not related to each other. Therefore, it is recommended that you read this article first to later address this AbstractFactory pattern.
The Abstract Factory pattern allows for a clearer code, since it avoids the previously mentioned problem. The UML diagram of this pattern is as follows:
The classes that comprise this pattern are the following:
-
AbstractProductA and AbstractProductB are the interfaces for a set of products of the same type but of a different family. In other words, all the products that implement the
AbstractProductA
class belong to the same product type, although they will be organized into different families. This type of object will be better understood in the concrete example that follows. -
ProductA1, ProductA2, ProductB1 and ProductB are concrete implementations of each type of
AbstractProduct
. -
AbstractFactory is the interface that declares the set of creation methods for each of the concrete factories (
ConcreteFactory1
andConcreteFactory2
). -
ConcreteFactory1 and ConcreteFactory2 implement the creation methods of the
AbstractFactory
class for each of the product families.
Abstract Factory Pattern: When To Use
-
The problems solved by Abstract Factory are similar to those solved by the Factory-Method pattern, but with greater abstraction in the types of objects that need to be created. Therefore, in the case of Abstract Factory it is required to work with several families of products related to each other rather than in a set of products.
-
The family of objects with which the client must work is not known a priori. Rather, this knowledge depends directly on the interaction of another user with the system (end user or system).
-
In the event that it is necessary to extend the internal components (the number of families and objects that are created) without having to have the code coupled, but rather have interfaces and abstractions that allow to easily extend with factories and specific products.
Abstract Factory Pattern: Advantages and Disadvantages
The Abstract Factory pattern has a number of advantages that can be summarized in the following points:
-
Compatibility between products created by the same factory class is guaranteed.
-
Clean code as the Open-Closed Principle is guaranteed since new product families can be introduced without breaking the existing code.
-
Cleaner code since the Single Responsibility Principle (SRP) is respected since the responsibility of creating the concrete product is transferred to the concrete creator class instead of the client class having this responsibility.
-
Cleaner code because the Single Responsibility Principle (SRP) is respected since the responsibility of creating the concrete product is transferred to the concrete creator class instead of the client class having this responsibility.
However, the main drawback of the abstract factory pattern, like most design patterns, is that there is an increase in complexity in the code, and an increase in the number of classes required for the code. Although, this disadvantage is well known when applying design patterns for it is the price to pay for gaining abstraction in the code.
Abstract Factory Pattern Examples
Next, we are going to illustrate two examples of application of the Abstract Factory pattern:
-
Basic structure of the Abstract Factory pattern. In this example we are going to translate the theoretical UML diagram into TypeScript code to identify each of the classes involved in the pattern.
-
Creation of characters in a video game. Let's think of the classic WoW (World of Warcraft) in which the player can have a set of objects depending on the race they choose. For example, we will have the races: Humans, Orcs and Magicians; which will have weapons and armor (products) that will be different depending on the race (the family of objects).
The following examples will show the implementation of this pattern using TypeScript. We have chosen TypeScript to carry out this implementation rather than JavaScript — the latter lacks interfaces or abstract classes so the responsibility of implementing both the interface and the abstract class would fall on the developer.
Example 1: Basic structure of the abstract factory pattern
In this first example, we’re going to translate the theoretical UML diagram into TypeScript to test the potential of this pattern. This is the diagram to be implemented:
First, we are going to define the interfaces (AbstractProductA
and AbstractProductB
) that define the types of concrete products that we want to create for the different families. In our concrete example, to simplify the understanding of the pattern as much as possible, only one method has been defined for each of these interfaces: usefulFunctionA
and usefulFunctionB
respectively.
export interface AbstractProductA {
usefulFunctionA(): string;
}
export interface AbstractProductB {
usefulFunctionB(): string;
}
The next step is to define the specific products that implement each of these interfaces. In our case, two concrete objects will be implemented for each of these abstract classes. For the first interface (AbstractProductA
) the classes ConcreteProductA1
and ConcreteProductA2
are implemented, while for the second interface (AbstractProductB
) the classes ConcreteProductB1
and ConcreteProductB2
are implemented.
import { AbstractProductA } from "./abstract-productA";
export class ConcreteProductA1 implements AbstractProductA {
public usefulFunctionA(): string {
return "The result of the product A1.";
}
}
import { AbstractProductA } from "./abstract-productA";
export class ConcreteProductA2 implements AbstractProductA {
public usefulFunctionA(): string {
return "The result of the product A2.";
}
}
import { AbstractProductB } from "./abstract-productB";
export class ConcreteProductB1 implements AbstractProductB {
public usefulFunctionB(): string {
return "The result of the product B1.";
}
}
import { AbstractProductB } from "./abstract-productB";
export class ConcreteProductB2 implements AbstractProductB {
public usefulFunctionB(): string {
return "The result of the product B2.";
}
}
Once the structure of classes related to the creation of products has been defined, we proceed to define the structure of classes related to the creation of factories in charge of creating these objects. Therefore, first the abstract class AbstractFactory
is defined in which the methods in charge of creating the concrete objects by the concrete factories are defined. However, note that these methods return the abstract classes from each of the AbstractProductA
and AbstractProductB
products.
import { AbstractProductA } from "./abstract-productA";
import { AbstractProductB } from "./abstract-productB";
export interface AbstractFactory {
createProductA(): AbstractProductA;
createProductB(): AbstractProductB;
}
Finally, it would be necessary to define the concrete factories, in which the concrete classes are instantiated. In this first example, the ConcreteFactory1
factory will be in charge of instantiating the concrete objects of family 1 (ConcreteProductA1
and ConcreteProductB1
) and the ConcreteFactory2
factory will be in charge of instantiating the concrete objects of family 2 (ConcreteProductA2
and ConcreteProductB2
).
import { AbstractFactory } from "./abstract-factory";
import { AbstractProductA } from "./abstract-productA";
import { AbstractProductB } from "./abstract-productB";
import { ConcreteProductA1 } from "./concrete-productA1";
import { ConcreteProductB1 } from "./concrete-productB1";
export class ConcreteFactory1 implements AbstractFactory {
public createProductA(): AbstractProductA {
return new ConcreteProductA1();
}
public createProductB(): AbstractProductB {
return new ConcreteProductB1();
}
}
import { AbstractFactory } from "./abstract-factory";
import { AbstractProductA } from "./abstract-productA";
import { AbstractProductB } from "./abstract-productB";
import { ConcreteProductA2 } from "./concrete-productA2";
import { ConcreteProductB2 } from "./concrete-productB2";
export class ConcreteFactory2 implements AbstractFactory {
public createProductA(): AbstractProductA {
return new ConcreteProductA2();
}
public createProductB(): AbstractProductB {
return new ConcreteProductB2();
}
}
Although it is not a direct part of the pattern, it would be necessary to see the execution of the pattern by the Client/Context
class. In this case, the ClientCode
method does not need to know the specific factory to create the products, but receiving an object of the AbstractFactory
class as a parameter is sufficient to execute the CreateProductA
and CreateProductB
methods.
import { AbstractFactory } from "./abstract-factory";
import { ConcreteFactory1 } from "./concrete-factory1";
import { ConcreteFactory2 } from "./concrete-factory2";
function clientCode(factory: AbstractFactory) {
const productA = factory.createProductA();
const productB = factory.createProductB();
console.log(productA.usefulFunctionA());
console.log(productB.usefulFunctionB());
}
console.log("Client: Testing client code with ConcreteFactory1");
clientCode(new ConcreteFactory1());
console.log("----------------");
console.log("Client: Testing the same client code with ConcreteFactory2");
clientCode(new ConcreteFactory2());
Example 2 - Creation of Heroes equipment of a video game
We have already seen the theoretical example of this pattern, so you already understand the responsibilities of each of the classes of this pattern. Now, we are going to illustrate a real example in which we will identify each of the classes of this design pattern.
Our problem consists of the representation of the equipment of different heroes or characters in a video game. We will focus on the classic WoW video game (World of Warcraft), in which the heroes are divided into three races: Humans, orcs and wizards. Each of these heroes can have different armor (armor
) and weapons (weapon
) that vary depending on the race. Therefore, we can already identify that the products to be built will be the different types of armor and weapons, and the product families are the product family for a human, orc and wizard.
Therefore, following the same methodology as the one we have presented in the previous examples, we are going to start by looking at the UML diagram that will help us identify each of the parts of this pattern.
A priori, the class design of this problem may be impressive, but if we have understood the example of the basic structure of this pattern, we will understand this example perfectly.
We will start by creating each of the specific product types. That is, the first thing that is defined is the interface that models a weapon (weapon
).
export interface Weapon {
usefulFunction(): string;
}
To simplify the example, only one method called usefulFunction
has been defined for each one of the weapons
. Thus, the specific weapons that are defined are sword
, axe
and mage-fireball
.
import { Weapon } from "./weapon.interface";
export class Sword implements Weapon {
public usefulFunction(): string {
return "The result of the Sword";
}
}
import { Weapon } from "./weapon.interface";
export class Axe implements Weapon {
public usefulFunction(): string {
return "The result of the Axe";
}
}
import { Weapon } from "./weapon.interface";
export class MageFireball implements Weapon {
public usefulFunction(): string {
return "The result of the MageFireball";
}
}
In the same way that the weapon
has been defined, the different armor (armor
) is defined. In this specific case, we have created a collaboration between the armor (armor
) and the weapon (weapon
) through a method called usefulFunctionWithWeapon
to illustrate that the objects can be related to each other. The most important thing to note is that the collaborator parameter is of the abstract class Weapon
, rather than working with concrete classes.
import { Weapon } from "../weapons/weapon.interface";
export interface Armor {
usefulFunction(): string;
usefulFunctionWithWeapon(collaborator: Weapon): string;
}
The specific armors that we need for our problem are BodyArmor
,OrcArmor
and Cloak
that will be created by each of the object families according to the Hero's race.
import { Armor } from "./armor-interface";
import { Weapon } from "../weapons/weapon.interface";
export class BodyArmor implements Armor {
public usefulFunction(): string {
return "The result of the BodyArmor";
}
public usefulFunctionWithWeapon(collaborator: Weapon): string {
const result = collaborator.usefulFunction();
return `The result of the BodyAmor collaborating with the (${result})`;
}
}
import { Armor } from "./armor-interface";
import { Weapon } from "../weapons/weapon.interface";
export class OrcArmor implements Armor {
public usefulFunction(): string {
return "The result of the OrcArmor";
}
public usefulFunctionWithWeapon(collaborator: Weapon): string {
const result = collaborator.usefulFunction();
return `The result of the OrcAmor collaborating with the (${result})`;
}
}
import { Armor } from "./armor-interface";
import { Weapon } from "../weapons/weapon.interface";
export class Cloak implements Armor {
public usefulFunction(): string {
return "The result of the Cloak";
}
public usefulFunctionWithWeapon(collaborator: Weapon): string {
const result = collaborator.usefulFunction();
return `The result of the Cloak collaborating with the (${result})`;
}
}
Up to this point, the specific products that we want to create in our video game have been defined but the creation rules have not been established. It is the specific factories that will be in charge of creating the specific products according to the Hero's race. The first class to define is the abstract class AbstractFactory
which defines the createWeapon
and createAmor
methods that are responsible for creating the abstract Weapon
and Armor
products. Notice that all the code up to this point has made use of abstract classes.
import { Armor } from "./armor/armor-interface";
import { Weapon } from "./weapons/weapon.interface";
export interface AbstractFactory {
createWeapon(): Weapon;
createArmor(): Armor;
}
At this time, we have to implement the concrete factories HumanFactory
, OrcFactory
and MageFactory
in which the creator methods are implemented with the concrete products based on the race of the hero.
import { AbstractFactory } from "./abstract-factory";
import { Armor } from "./armor/armor-interface";
import { BodyArmor } from "./armor/body-armor.model";
import { Sword } from "./weapons/sword.model";
import { Weapon } from "./weapons/weapon.interface";
export class WarriorFactory implements AbstractFactory {
public createWeapon(): Weapon {
return new Sword();
}
public createArmor(): Armor {
return new BodyArmor();
}
}
import { AbstractFactory } from "./abstract-factory";
import { Armor } from "./armor/armor-interface";
import { Axe } from "./weapons/axe.model";
import { OrcArmor } from "./armor/orc-armor.model";
import { Weapon } from "./weapons/weapon.interface";
export class OrcFactory implements AbstractFactory {
public createWeapon(): Weapon {
return new Axe();
}
public createArmor(): Armor {
return new OrcArmor();
}
}
import { AbstractFactory } from "./abstract-factory";
import { Armor } from "./armor/armor-interface";
import { Cloak } from "./armor/cloak.model";
import { MageFireball } from "./weapons/mage-fireball.model";
import { Weapon } from "./weapons/weapon.interface";
export class MageFactory implements AbstractFactory {
public createWeapon(): Weapon {
return new MageFireball();
}
public createArmor(): Armor {
return new Cloak();
}
}
To conclude the example of creating the equipment of our heroes, we are going to implement the Client/Context
class.
import { AbstractFactory } from "./abstract-factory";
import { MageFactory } from "./mage-factory";
import { OrcFactory } from "./orc-factory";
import { WarriorFactory } from "./warrior-factory";
function clientCode(factory: AbstractFactory) {
const sword = factory.createWeapon();
const armor = factory.createArmor();
console.log(armor.usefulFunction());
console.log(armor.usefulFunctionWithWeapon(sword));
}
console.log("Client: WarriorFactory");
clientCode(new WarriorFactory());
console.log("----------------");
console.log("Client: OrcFactory");
clientCode(new OrcFactory());
console.log("----------------");
console.log("Client: MageFactory");
clientCode(new MageFactory());
Finally, I’ve created two npm scripts
, through which the code presented in this article can be executed:
npm run example1
npm run example2
GitHub Repo available here.
Conclusion
Abstract Factory is a design pattern that allows respecting the Open-Closed Principle principle and delegates the responsibility for creating objects to specific classes (concrete factories) using polymorphism. This allows us to have a much cleaner and more scalable code.
This pattern solves the problem that arises when it is necessary to create different types of objects that depend on the interaction of a client with the system in which it is not known beforehand which object the client will create. Furthermore, these objects are related by object families, in such a way that it allows to have them separated by context or object types when using different factories.
Another advantage of this pattern is that the system is not coupled to a set of concrete classes, but the client only communicates with abstract classes allowing to have a much more maintainable code when the software scales.
Finally, the most important thing about this pattern is not the concrete implementation of it, but being able to recognize the problem that this pattern can solve, and when it can be applied. The specific implementation is the least of it, since it will vary depending on the programming language used.