DEV Community

Cover image for Understanding Design Patterns: Factory-Method
Carlos Caballero
Carlos Caballero

Posted on • Originally published at carloscaballero.io

Understanding Design Patterns: 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:

  1. 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.

  2. The factoryManager class is attached to all types of objects that you want to build, creating code known as spaghetti 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();
  }
}
Enter fullscreen mode Exit fullscreen mode

The Factory-Method pattern allows for clearer code, since it avoids the problem raised above. The UML diagram of this pattern is as follows:

UML Diagram from Design Patterns: Elements of Reusable Object-Oriented Software book.

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 type Product. The concrete implementation of the object is not carried out by this class, but responsibility is delegated to the ConcreteCreator1 and ConcreteCreator2 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:

  1. 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.

  2. This pattern is a specific case of the Template-Method pattern, 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.

    1. 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 the Product interface.

Factory-Method Pattern: When To Use

  1. 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.

  2. 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:

  1. 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.

  2. 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.

  3. 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:

Class diagram of the basic structure of the factory-method pattern.

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;
}
Enter fullscreen mode Exit fullscreen mode

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";
  }
}
Enter fullscreen mode Exit fullscreen mode
import { Product } from "./product.interface";

export class ConcreteProduct2 implements Product {
  public operation(): string {
    return "ConcreteProduct2: Operation";
  }
}
Enter fullscreen mode Exit fullscreen mode

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()}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode
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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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:

  1. High coupling of the ProductManager class with the system.

  2. Spaghetti code in the createProduct method of the ProductManager class which is built with a switch-case that breaks the Open-Closed Principle when you want to extend to other types of products.

Simple Factory.

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;
}
Enter fullscreen mode Exit fullscreen mode

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";
  }
}
Enter fullscreen mode Exit fullscreen mode
import { Product } from "./product.interface";

export class Kebab implements Product {
    public operation(): string {
        return 'Kebab: Operation';
    }
}
Enter fullscreen mode Exit fullscreen mode
import { Product } from "./product.interface";

export class Pizza implements Product {
    public operation(): string {
        return 'Pizza: Operation';
    }
}
Enter fullscreen mode Exit fullscreen mode

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!");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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.

Factoy-Method

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;
}
Enter fullscreen mode Exit fullscreen mode
import { Product } from "./product.interface";

export class Burger implements Product {
  public operation(): string {
    return "Burger: Results";
  }
}
Enter fullscreen mode Exit fullscreen mode
import { Product } from "./product.interface";

export class Kebab implements Product {
    public operation(): string {
        return 'Kebab: Operation';
    }
}
Enter fullscreen mode Exit fullscreen mode
import { Product } from "./product.interface";

export class Pizza implements Product {
    public operation(): string {
        return 'Pizza: Operation';
    }
}
Enter fullscreen mode Exit fullscreen mode

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()}`;
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode
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();
    }
}
Enter fullscreen mode Exit fullscreen mode
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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)