Understanding Design Patters: Factory Method
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 Factory-Method Pattern works and when it should be applied.
Factory-Method: Basic Idea
The factory method pattern is a creational pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created. This is done by creating objects by calling a factory method — either specified in an interface and implemented by child classes, or implemented in a base class and optionally overridden by derived classes — rather than by calling a constructor — Wikipedia
Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses — 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. 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 leads to 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 asspaghetti code
.
The following code shows the classic problem in which there is a create
method that returns an object of a type based on a parameter pass as an argument:
function create(type) {
switch(type){
case '0': return new Object1();
case '1': return new Object2();
case '2': return new Object3();
default: return new Object4();
}
}
The Factory-Method pattern allows for clearer code, since it avoids the problem raised above. The UML diagram of this pattern is as follows:
The classes that make up this pattern are the following:
-
Product it is the common interface of all objects that can be created.
-
ConcreteProductOne and ConcreteProductTwo are implementations of the
Product
interface. -
Creator is an abstract class in which the
factoryMethod
method is declared, which will be responsible for generating an object of typeProduct
. The concrete implementation of the object is not carried out by this class, but responsibility is delegated to theConcreteCreator1
andConcreteCreator2
classes. -
ConcreteCreator1 and ConcreteCreator2 override the
factoryMethod
with the creation of the concrete object.
It is important to clarify several points that are often misunderstood as a result of the name of this pattern:
-
This pattern does not implement a
factory
method that is responsible for creating specific objects. Rather, the responsibility is delegated to the subclasses that implement the abstract class. -
This pattern is a specific case of the [Template-Method pattern] (/design-patterns-template-method/), in which it delegates the responsibility of variants in an algorithm to concrete classes. In the case of the Factory-Method pattern, the responsibility of creating objects is being delegated to the classes that implement the interface.
-
The
factoryMethod
method does not have to create new instances every time, but can return these objects from a memory cache, local storage, etc. What is important is that this method must return an object that implements theProduct
interface.
Factory-Method Pattern: When To Use
-
The problem solved by the pattern Factory-Method is easy to identify: The object with which the client must work is not known a priori, but this knowledge depends directly on the interaction of another user with the system (end-user or system). The traditional example where the need for this pattern arises is when the user selects an object type from a list of options.
-
In the event that it is necessary to extend the internal components (the number of objects that are created) without the need to have the code attached, but instead there is an interface that must be implemented and it should only be extended by creating a class relative to the new object to be included and its specific creator.
Factory-Method Pattern: Advantages and Disadvantages
The Factory-Method pattern has a number of advantages that can be summarized in the following points:
-
The code is more maintainable because it is less coupled between the client classes and their dependencies.
-
Clean code since the Open-Closed Principle is guaranteed due to new concrete classes of
Product
can be introduced without having to break the existing code. -
Cleaner code since the Single Responsibility Principle (SRP) is respected because 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 factory-method pattern is the increased complexity in the code and the increased number of classes required. This a well-known disadvantage when applying design patterns — the price that must be paid to gain abstraction in the code.
Factory-Method pattern examples
Next we are going to illustrate two examples of application of the Factory-Method pattern:
-
Basic structure of the Factory-Method pattern. In this example, we'll translate the theoretical UML diagram into TypeScript code in order to identify each of the classes involved in the pattern.
-
A Point of Service (POS) of a fast food restaurant in which the Factory-Method pattern will be incorrectly applied resulting in a software pattern (not by design) known as Simple-Factory in which the Open-Closed Principle is not respected. However, this programming technique is really useful when no more abstraction is required than necessary. Although, the price to pay is high when you want to scale the project.
-
Resolution of the previous problem applying the Factory-Method pattern.
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 Factory-Method 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 of all, we are going to define the interface (Product
) of our problem. As it is an interface, all the methods that must be implemented in all the specific products (ConcreteProduct1
and ConcreteProduct2
) are defined. Therefore, the Product
interface in our problem is quite simple, as shown below:
export interface Product {
operation(): string;
}
The objects that we want to build in our problem must implement the previously defined interface. Therefore, concrete classes ConcreteProduct1
and ConcreteProduct2
are created which satisfy the Product
interface and implement the operation
method.
import { Product } from "./product.interface";
export class ConcreteProduct1 implements Product {
public operation(): string {
return "ConcreteProduct1: Operation";
}
}
import { Product } from "./product.interface";
export class ConcreteProduct2 implements Product {
public operation(): string {
return "ConcreteProduct2: Operation";
}
}
The next step is to define the Creator
abstract class in which an abstract factoryMethod
must be defined, which is the one that will be delegated to the concrete classes for the creation of an instance of a concrete object. The really important thing is that it must return an object of the Product
class.
On the other hand, the operation method has been defined which makes use of the factoryMethod
abstract method. The factoryMethod
method that is executed will be that of the concrete class in which it is defined.
import { Product } from "./product.interface";
export abstract class Creator {
protected abstract factoryMethod(): Product;
public operation(): string {
const product = this.factoryMethod();
return `Creator: ${product.operation()}`;
}
}
The classes responsible for creating concrete objects are called ConcreteCreator
. Each of the ConcreteCreator
classes implement the factoryMethod
method in which a new object of the ConcreteProduct1
or ConcreteProduct2
class is created depending on the creator
class that has been used.
import { ConcreteProduct1 } from "./concrete-product1";
import { Creator } from "./creator";
import { Product } from "./product.interface";
export class ConcreteCreator1 extends Creator {
protected factoryMethod(): Product {
return new ConcreteProduct1();
}
}
import { ConcreteProduct2 } from "./concrete-product2";
import { Creator } from "./creator";
import { Product } from "./product.interface";
export class ConcreteCreator2 extends Creator {
protected factoryMethod(): Product {
return new ConcreteProduct2();
}
}
Finally, we would see how the class Client
or Context
can select which objects created without prior knowledge, and how this pattern keeps the Open-Closed Principle (OCP).
import { ConcreteCreator1 } from "./concrete-creator1";
import { ConcreteCreator2 } from "./concrete-creator2";
import { Creator } from "./creator";
function client(creator: Creator) {
console.log(`Client: I'm not aware of the creator's class`);
console.log(creator.operation());
}
const concreteCreator1 = new ConcreteCreator1();
const concreteCreator2 = new ConcreteCreator2();
client(concreteCreator1);
console.log("----------");
client(concreteCreator2);
Example 2 - POS of a Restaurant (Simple-Factory)
In this example, a solution will be developed that does not satisfy the Factory-Method pattern but uses a FactoryManager class that is responsible for building any object. This solution breaks with the Open-Closed Principle, in addition to having spaghetti code in the creation of objects. The interesting thing is that this same example is refactored into the following example using the factory-method pattern.
The solution proposed here is not a design pattern, but it is a solution that is widely used in the industry. In fact, it has been called Simple Factory and has serious problems as the application scales.
The application to be built is a simple application that allows you to create different types of objects: Pizza
, Burger
or Kebab
.
The creation of these objects is not known a priori and depends on user interaction. The ProductManager
class is in charge of building an object of a certain class through the createProduct
method.
Below is the UML diagram of this first proposal. A priori the two problems of this solution are already observed:
-
High coupling of the
ProductManager
class with the system. -
Spaghetti code in the
createProduct
method of theProductManager
class which is built with aswitch-case
that breaks the Open-Closed Principle when you want to extend to other types of products.
As in other examples, we will gradually show the code for the implementation of this solution. The Product interface is exactly the same as the one used in the solution proposed by the Factory-Method pattern.
export interface Product {
operation(): string;
}
The next step consists of the implementation of each of the specific objects that you want to create in this problem: Burger
, Kebab
and Pizza
.
import { Product } from "./product.interface";
export class Burger implements Product {
public operation(): string {
return "Burger: Results";
}
}
import { Product } from "./product.interface";
export class Kebab implements Product {
public operation(): string {
return 'Kebab: Operation';
}
}
import { Product } from "./product.interface";
export class Pizza implements Product {
public operation(): string {
return 'Pizza: Operation';
}
}
Finally, we implement the ProductManager
class, which is responsible for creating each of the object types based on the type parameter. An enum type has been used that allows us to avoid using strings in the use of the switch-case
statement.
import { Burger } from "./burger.model";
import { Kebab } from "./kebab.model";
import { PRODUCT_TYPE } from "./product-type.enum";
import { Pizza } from "./pizza.model";
export class ProductManager {
constructor() {}
createProduct(type): Product {
switch (type) {
case PRODUCT_TYPE.PIZZA:
return new Pizza();
case PRODUCT_TYPE.KEBAB:
return new Kebab();
case PRODUCT_TYPE.BURGER:
return new Burger();
default:
throw new Error("Error: Product invalid!");
}
}
}
Finally, it would be necessary to show the Client
or Context
class that makes use of the productManager
class. Apparently from the Client
class it is not observed that under this class there is a strongly coupled code that violates the principles of clean code.
import { PRODUCT_TYPE } from "./product-type.enum";
import { ProductManager } from "./product-manager";
const productManager = new ProductManager();
const burger = productManager.createProduct(PRODUCT_TYPE.BURGER);
const pizza = productManager.createProduct(PRODUCT_TYPE.PIZZA);
const kebab = productManager.createProduct(PRODUCT_TYPE.KEBAB);
console.log(burger.operation());
console.log(pizza.operation());
console.log(kebab.operation());
Example 3 - POS of a Restaurant using Factory-Method
In this example, we are going to take up the problem posed in Example 2 (POS of a restaurant) to propose the solution using the factory-method pattern. The objective of this solution is to avoid the spaghetti code that has been generated in the productManager
class and to allow respecting the Open-Closed Principle.
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.
In this case, the objects that we want to build would be those corresponding to the Pizza
, Burger
and Kebab
classes. These classes implement the Product
interface. All this part of code is identical to the one presented in the previous example. However, let's review the code to keep it in mind:
export interface Product {
operation(): string;
}
import { Product } from "./product.interface";
export class Burger implements Product {
public operation(): string {
return "Burger: Results";
}
}
import { Product } from "./product.interface";
export class Kebab implements Product {
public operation(): string {
return 'Kebab: Operation';
}
}
import { Product } from "./product.interface";
export class Pizza implements Product {
public operation(): string {
return 'Pizza: Operation';
}
}
On the other side of the UML diagram, we can find the creator
classes. Let's start by reviewing the Creator
class, which is responsible for defining the factoryMethod
method, which must return an object that implements the Product
interface. In addition, we will have the someOperation
method which makes use of the factoryMethod
abstract method which is developed in each of the concrete creator classes.
import { Product } from "./product.interface";
export abstract class Creator {
public abstract factoryMethod(): Product;
public someOperation(): string {
const product = this.factoryMethod();
return `Creator: The same creator's code has just worked with ${product.operation()}`;
}
}
We would still have to define each of the specific BurgerCreator
, KebabCreator
and PizzaCreator
creator classes that will create each of the specific objects (NOTE: remember that it is not necessary to always create an object, if we had a structure of data from which instances that were cached were retrieved, the pattern would also be implemented).
import { Creator } from "./creator";
import { Kebab } from "./kebab.model";
import { Product } from "./product.interface";
export class KebabCreator extends Creator {
public factoryMethod(): Product {
return new Kebab();
}
}
import { Creator } from "./creator";
import { Pizza } from "./pizza.model";
import { Product } from "./product.interface";
export class PizzaCreator extends Creator {
public factoryMethod(): Product {
return new Pizza();
}
}
import { Burger } from "./burger.model";
import { Creator } from "./creator";
import { Product } from "./product.interface";
export class BurgerCreator extends Creator {
public factoryMethod(): Product {
return new Burger();
}
}
The last step we would have to complete our example would be to apply the pattern that we have developed using it from the Client
or Context
class. It is important to note that the Client
function does not require any knowledge of the Creator
or the type of object to be created. Allowing to fully delegate responsibility to specific classes.
import { BurgerCreator } from "./burger-creator";
import { Creator } from "./creator";
import { KebabCreator } from "./kebab-creator";
import { PizzaCreator } from "./pizza-creator";
function client(creator: Creator) {
console.log('Client: I\'m not aware of the creator\'s class, but it still works.');
console.log(creator.someOperation());
}
const pizzaCreator = new PizzaCreator();
const burgerCreator = new BurgerCreator();
const kebabCreator = new KebabCreator();
console.log('App: Launched with the PizzaCreator');
client(pizzaCreator);
console.log('----------');
console.log('App: Launched with the BurgerCreator');
client(burgerCreator);
Finally, I have created three npm scripts
through which the code presented in this article can be executed:
npm run example1
npm run example2
npm run example3
GitHub Repo: https://github.com/Caballerog/blog/tree/master/factory-method-pattern
Conclusion
Factoy-Method is a design pattern that allows respecting the Open-Closed Principle and delegates the responsibility for creating objects to specific classes using polymorphism. This allows us to have a much cleaner and more scalable code. It mainly 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, and that it is not known a priori which object the client will create.
Finally, the most important thing about this pattern is not the specific 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 that will vary depending on the programming language used.