DEV Community

longtk26
longtk26

Posted on

Understanding the Factory Method Pattern

Introduction

Hi everyone, I am writing this post to share my knowledge as I continue learning about design patterns. Today, I will present the Factory Method Pattern, which is a design pattern commonly used in real-world applications. If there are any mistakes in my post, please feel free to comment below, and I will gladly fix and update it.

Factory method pattern provides an interface for creating objects in a superclass, but allow subclasses to alter the type of objects that will be created.

Problem

Assume you have a bank application, and you’re building a feature for transferring money through various methods like bank transfer, paypal transfer,…

Before using the Factory Method pattern, let’s examine the scenario without it.

I will give an example implemented in Java.

Situation: Person1 sends money to Person2 using a transfer method (Bank Transfer or PayPal Transfer).

Folder structure:

problem/
├─ BankApp.java
├─ service/
│  ├─ PaypalTransferPayment.java
│  ├─ BankTransferPayment.java
├─ data/
│  ├─ Person.java
Enter fullscreen mode Exit fullscreen mode

In the main application, create two persons with default amounts of money.

package problem;

import problem.data.Person;

public class BankApp {
    public static void main(String[] args) {
        Person person1 = new Person("John", 1000);
        Person person2 = new Person("Jane", 500);
    }
}
Enter fullscreen mode Exit fullscreen mode

Create BankTransferPayment and PaypalTransferPayment classes.

package problem.service;

import problem.data.Person;

public class BankTransferPayment {
    public void processPayment(Person fromAccount, Person toAccount, float amount) {
        fromAccount.withdraw(amount);
        toAccount.deposit(amount);
        System.out.println("Bank transfer payment success.");
    }
}
Enter fullscreen mode Exit fullscreen mode
package problem.service;

import problem.data.Person;

public class PaypalPayment {
    public void processPayment(Person fromAccount, Person toAccount, float amount) {
        fromAccount.withdraw(amount);
        toAccount.deposit(amount);
        System.out.println("Paypal transfer payment success.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Implement the logic in the main function.

package problem;

import problem.data.Person;
import problem.service.BankTransferPayment;
import problem.service.PaypalPayment;

public class BankApp {
    public static void main(String[] args) {
        Person person1 = new Person("John", 1000);
        Person person2 = new Person("Jane", 500);

        String paymentMethod = "BANK_TRANSFER";

        if (paymentMethod.equals("BANK_TRANSFER")) {
            BankTransferPayment bankTransferPayment = new BankTransferPayment();
            bankTransferPayment.processPayment(person1, person2, 100);

            System.out.println("===Method bank_transfer===");
            System.out.println(person1.getName() + " has " + person1.getAmount());
            System.out.println(person2.getName() + " has " + person2.getAmount());
        } else if (paymentMethod.equals("PAYPAL")) {
            PaypalPayment paypalPayment = new PaypalPayment();
            paypalPayment.processPayment(person1, person2, 100);

            System.out.println("===Method paypal===");
            System.out.println(person1.getName() + " has " + person1.getAmount());
            System.out.println(person2.getName() + " has " + person2.getAmount());
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Problems with the current implementation:

  1. Repetitive code: The processPayment method logic is repeated for every payment method.
  2. Tightly coupled code: The application needs to create the payment method objects itself, making it hard to extend the application.
  3. Scalability issues: If new payment methods are added, the source code becomes more complex and harder to maintain.

Solution

The solution to the above situation is to use factory method pattern. So, how do we apply it ?

In the example above:

  1. Each if-else block calls the processPayment method, which leads to repetitive code.
  2. Objects are created based on the payment type condition, making the code messy with excessive if-else statements.

To solve these issues, the Factory Method pattern will be implemented step by step.

Folder structure (solution):

solution/
├─ BankApp.java
├─ service/
  ├─ payments/
    ├─ Payment.java
    ├─ PaymentFactory.java
    ├─ BankTransferPayment.java
    ├─ PaypalTransferPayment.java
├─ data/
  ├─ Person.java
Enter fullscreen mode Exit fullscreen mode

Step 1: Create Payment interface, declares common method processPayment

package solution.service.payments;

import solution.data.Person;

// Step 1: Create an interface for the payment
public interface Payment {
    void processPayment(Person fromAccount, Person toAccount,float amount);
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create BankTransferPayment and PaypalTransferPayment classes implement Payment interface.

package solution.service.payments;

import solution.data.Person;

// Step 2: Create a class that implements the Payment interface
public class BankTransferPayment implements Payment {
    @Override
    public void processPayment(Person fromAccount, Person toAccount, float amount) {
        fromAccount.withdraw(amount);
        toAccount.deposit(amount);
        System.out.println("Bank transfer payment success.");
    }
}
Enter fullscreen mode Exit fullscreen mode
package solution.service.payments;

import solution.data.Person;

public class PaypalPayment implements Payment{
    @Override
    public void processPayment(Person fromAccount, Person toAccount, float amount) {
        fromAccount.withdraw(amount);
        toAccount.deposit(amount);
        System.out.println("Paypal transfer payment success.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create PaymentFactory class. This class is responsible for creating objects based on payment type condition.

package solution.service.payments;

public class PaymentFactory {
    public Payment createPayment(String paymentType) {
        if (paymentType == null) {
            return null;
        }
        if (paymentType.equalsIgnoreCase("BANK_TRANSFER")) {
            return new BankTransferPayment();
        } else if (paymentType.equalsIgnoreCase("PAYPAL")) {
            return new PaypalPayment();
        }
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Use the Factory in the Main Application.

Modify the main function to use the Factory Method pattern.

package solution;

import solution.data.Person;
import solution.service.payments.Payment;
import solution.service.payments.PaymentFactory;

public class BankApp {
    public static void main(String[] args) {
        Person person1 = new Person("John", 1000);
        Person person2 = new Person("Jane", 500);

        String paymentMethod = "PAYPAL";

        Payment payment = new PaymentFactory().createPayment(paymentMethod);
        payment.processPayment(person1, person2, 100);
    }
}
Enter fullscreen mode Exit fullscreen mode

Benefits of Using the Factory Method Pattern

  • The code is cleaner and more structured.
  • Repetitive calls to processPayment in multiple if-else blocks are eliminated.
  • Object creation is delegated to the factory, improving maintainability.

Bonus

To make the PaymentFactory class comply with the Open/Closed Principle (from SOLID principles), you can implement a dynamic registration mechanism using the Strategy Pattern.

Updated PaymentFactory.java:

package solution.service.payments;

import java.util.HashMap;
import java.util.Map;

public class PaymentFactory {
    private Map<String, Payment> paymentMaps;

    public PaymentFactory() {
        this.paymentMaps = new HashMap<>();
    }

    public Payment createPayment(String paymentType) {
        return paymentMaps.get(paymentType);
    }

    public void registerPayment(String paymentType, Payment payment) {
        paymentMaps.put(paymentType, payment);
    }

    public PaymentFactory initializePaymentMethods() {
        Payment bankTransferPayment = new BankTransferPayment();
        Payment paypalPayment = new PaypalPayment();

        this.registerPayment("BANK_TRANSFER", bankTransferPayment);
        this.registerPayment("PAYPAL", paypalPayment);

        return this;
    }
}
Enter fullscreen mode Exit fullscreen mode

Using the Updated Factory in the Main Application.

package solution;

import solution.data.Person;
import solution.service.payments.Payment;
import solution.service.payments.PaymentFactory;

public class BankApp {
    public static void main(String[] args) {
        Person person1 = new Person("John", 1000);
        Person person2 = new Person("Jane", 500);

        String paymentMethod = "BANK_TRANSFER";

        Payment payment = new PaymentFactory()
                .initializePaymentMethods()
                .createPayment(paymentMethod);
        payment.processPayment(person1, person2, 100);
    }
}

Enter fullscreen mode Exit fullscreen mode

By applying this approach, the code adheres to the Open/Closed Principle, enabling the addition of new payment methods without modifying the PaymentFactory logic.

I hope this post will be helpful to you.

References:

guru-design-patterns

Top comments (0)