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
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);
}
}
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.");
}
}
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.");
}
}
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());
}
}
}
Problems with the current implementation:
-
Repetitive code: The
processPayment
method logic is repeated for every payment method. - Tightly coupled code: The application needs to create the payment method objects itself, making it hard to extend the application.
- 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:
- Each
if-else
block calls theprocessPayment
method, which leads to repetitive code. - 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
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);
}
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.");
}
}
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.");
}
}
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;
}
}
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);
}
}
Benefits of Using the Factory Method Pattern
- The code is cleaner and more structured.
- Repetitive calls to
processPayment
in multipleif-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;
}
}
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);
}
}
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.
Top comments (0)