Design patterns are often explained in a very complicated way, which makes it hard for casual readers to understand.
So today, I would like to explain to you how I understand the Factory Method Pattern in the simplest way possible—and with a real-world example!
The problem we are trying to solve
Let's assume we are trying to support multiple payment methods in our app. It should be easy but in reality it is very cumbersome to implement. All the methods allow users to do the same operations (pay for an order, get a refund, etc.), but each service does it in a different way. You do not process a payment with Stripe in the same way you do with PayPal, for example.
Also, we want to be able to expand the code in the future without breaking the existing one. Our app may need to support Google Pay in the near future. Who knows!
The solution: The Factory Method
We first define the interface that all our payment methods have to follow. In this case, each payment method has to allow users to process a payment and do a refund. No matter if we use Stripe, PayPal or whatever, users have to be able to perform these two operations.
interface PaymentProcessor {
processPayment(
amount: number,
): Promise<{ success: boolean; transactionId: string }>;
refund(transactionId: string): Promise<boolean>;
}
Now we define each processor.
ℹ️ I use the word processor rather than method because in this code context I think it makes more sense.
class StripeProcessor implements PaymentProcessor {
async processPayment(
amount: number,
): Promise<{ success: boolean; transactionId: string }> {
console.log(`Processing $${amount} via Stripe...`);
// Stripe API call
return { success: true, transactionId: 'stripe_tx_12345' };
}
async refund(transactionId: string): Promise<boolean> {
console.log(`Refunding Stripe transaction ${transactionId}`);
return true;
}
}
class PayPalProcessor implements PaymentProcessor {
async processPayment(
amount: number,
): Promise<{ success: boolean; transactionId: string }> {
console.log(`Processing $${amount} via PayPal...`);
// PayPal API call
return { success: true, transactionId: 'paypal_tx_67890' };
}
async refund(transactionId: string): Promise<boolean> {
console.log(`Refunding PayPal transaction ${transactionId}`);
return true;
}
}
We have to also specify our creator, or in other words, the class that will have the factory method that will instantiate the specific processors and orchestrate their methods.
abstract class PaymentService {
abstract createProcessor(): PaymentProcessor; // The Factory Method
async checkout(amount: number): Promise<void> {
const processor = this.createProcessor();
const result = await processor.processPayment(amount);
if (result.success) {
console.log(
`Payment successful! Transaction ID: ${result.transactionId}`,
);
}
}
async refund(transactionId: string): Promise<void> {
const processor = this.createProcessor();
const success = await processor.refund(transactionId);
if (success) {
console.log(`Refund successful for Transaction ID: ${transactionId}`);
}
}
}
😨 Are you confused? Don't worry, bear with me; once we finish, you will understand.
Now we create the different implementations of the services by extending the creator class:
class StripePaymentService extends PaymentService {
createProcessor(): PaymentProcessor {
return new StripeProcessor();
}
}
class PayPalPaymentService extends PaymentService {
createProcessor(): PaymentProcessor {
return new PayPalProcessor();
}
}
And finally we apply it in our app:
function processCheckout(paymentMethod: 'stripe' | 'paypal', amount: number) {
const service =
paymentMethod === 'stripe'
? new StripePaymentService()
: new PayPalPaymentService();
service.checkout(amount);
}
processCheckout('stripe', 99.99);
Wait, I do not understand, what happened here?
We have created the classes StripePaymentService and PayPalPaymentService which extend the creator class PaymentService. This creator class uses a factory method createProcessor to create an instance of each the payment processor and use it to orchestrate the checkout and the refund.
Notice that the creator class does not care which payment processor are we using. It simply orchestrates the one it is provided.
Why is this pattern useful?
It allows us to execute different variants (Stripe, PayPal) of a process (processing payments and refunds) with minimal code changes.
We simply instantiate the class we need (StripePaymentService or PayPalPaymentService) and the creator class orchestrates all the different logic.
If in the future we want to add new payment processors it is also very easy to do: we simply create a new processor and extend the creator class (PaymentService).
Where does this pattern shine?
When we have a structure that is repeated multiple times with different implementations.
Like a payment system. The core functionality is the same for all the variants but the implementation changes significantly.
Where is this pattern used?
It is used for example in ORMs that support a lot of database types.
The ORM has the same functionality (query a database, create an item...) for all the databases, but the implementation changes a lot if we use PostgreSQL or MongoDB, for example.
Where can I learn more about this pattern?
Go to one of the most reputable resources to learn about architectural patterns. Refactoring Guru
Good bye!
Hope this article helped you! If you have any doubts, please let me know in the comments.
Top comments (0)