Factory - Creational Design Pattern

Today we will learn about the Factory Pattern and we will see that how beautifully a design pattern can reduce coupling and helps us writing maintainable code.

The Scenario

Assume that we are building an application where we are supposed to have multiple payment types. When the user tries to make an order the order module will provide us the amount that we need to change from the user. Now the user can have multiple payment methods that we need to pass to the Payment module for processing the order by deducting the charges from the payment method user has already provided.

We will first code the solution without using the Factory Pattern.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Payment {
pay(): void;
}

// concrete classes
class DebitCard implements Payment {
pay() {
console.log("payment using Debit Card");
return true;
}
}

class CreditCard implements Payment {
pay() {
console.log("payment using Credit Card");
return true;
}
}

Explanation

In the above code we have created an interface with a method Pay() returning void. It can throw exception if things go wrong like invalid details or insufficient balance. We have used the interface to facilitate polymorphism so that we can pass any entity having a Pay() method can be used for making payments. Now let’s see the main code of the Payment Service on how the payment is being taken.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class PaymentService {
processGetPayment(paymentType: string): void {
// payment option holder
let paymentOption: Payment | null;

switch (paymentType) {
case "dc":
paymentOption = new DebitCard();
break;
case "cc":
paymentOption = new CreditCard();
break;
default:
throw new Error("Unsupported payment type");
}

if (paymentOption !== null) {
paymentOption.pay()
}
}
}


function main() {
const ps = new PaymentService()
// pass the payment option for processing payment
ps.processGetPayment("w");
}

main();

Highlighting Issues

I think you have already sensed the issues in the above code. Let me highlight them:

  • We are exposing a lot of details to the PaymentService class unnecessarily.
  • The PaymentService class is tightly coupled with the payment options.
  • In future if we want to support more payment options we have to update the PaymentService class method(s).
  • The PaymentService‘s ProcessPayment should only focus on deducting payments not initiating the payment methods.
  • Also it’s anti-pattern (violation of open-close principle) to expose the initiation methods of cards to the PaymentService class.

Using Factory Pattern

To reduce the coupling and avoid exposing the details of the cards we will create a PaymentFactory class which will get the type of the payment user has selected and then return the payment type object after properly imitating it. Let’s do it.

Improved Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
interface Payment {
pay():void;
}

enum PaymentTypes {
DebitCard = "DebitCard",
CreditCard = "CreditCard",
}

class DebitCard implements Payment {
pay() {
console.log("payment using Debit Card");
}
}

class CreditCard implements Payment {
pay() {
console.log("payment using Credit Card");
}
}

class PaymentFactory {
static GetPayment(type: PaymentTypes): Payment {
let instance: Payment;

switch (type) {
case PaymentTypes.DebitCard:
instance = new DebitCard();
break;
case PaymentTypes.CreditCard:
instance = new CreditCard();
break;
default:
throw new Error("Unsupported payment type");
}

return instance
}
}

class PaymentService {
processGetPayment(paymentType: PaymentTypes): void {
let paymentOption = PaymentFactory.GetPayment(paymentType);

// process the payment
paymentOption.pay()
}
}


function main() {
const ps = new PaymentService()
// pass the payment option for processing payment
ps.processPayment(PaymentTypes.CreditCard);
ps.processPayment(PaymentTypes.DebitCard);
}

main();

Explanation

Now that we have the PaymentFactory with the static method to get the payment object, the PaymentService class can simply call it without taking the burden of object initialization. Also we are not exposing any details to the payment service class. On adding a new payment method there will be no need of modifying the code of the PaymentService class at all.

Let’s test the implementation by adding a new payment type - Wallet.

Adding Wallets

We will see the beauty of the Factory pattern by adding a new payment method called wallet. The steps will be to:

  • Update the enum to support wallet.
  • Add the concrete wallet class.
  • Wire the wallet with the payment factory
1
2
3
4
5
enum PaymentTypes {
DebitCard = "DebitCard",
CreditCard = "CreditCard",
Wallet = "Wallet",
}

The concrete wallet class:

1
2
3
4
5
class Wallet implements Payment {
pay() {
console.log("payment using Wallet");
}
}

Wiring up the wallet payment option with the payment factory class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class PaymentFactory {
static GetPayment(type: PaymentTypes): Payment | null {
let instance: Payment;

switch (type) {
case PaymentTypes.DebitCard:
instance = new DebitCard();
break;
case PaymentTypes.CreditCard:
instance = new CreditCard();
break;
case PaymentTypes.Wallet:
instance = new Wallet();
break;
default:
throw new Error("Unsupported payment type");
}

return instance
}
}

Note: As the types will increase the switch block will become bulky and error prone, we can use a map instead as the types grows.

We can now use the wallet method of payment in the main function as well:

1
2
3
4
5
6
7
function main() {
const ps = new PaymentService()
// pass the payment option for processing payment
ps.processPayment(PaymentTypes.CreditCard);
ps.processPayment(PaymentTypes.Wallet);
ps.processPayment(PaymentTypes.DebitCard);
}

The logs on execution will be:

1
2
3
payment using Credit Card
payment using Wallet
payment using Debit Card

Enums

You can see that we have done a bit of refactoring like using the enum PaymentTypes and also throwing error if the payment type was passed something else (This should not ideally happen because the enum will only have the types which are implemented in the Payment Factory). That’s how the use of enums make the code more robust.

Conclusion

The Factory pattern helps us create objects without exposing the creation logic to the business code. By using a factory, the code becomes cleaner, easier to maintain, and less tightly coupled. When new payment methods are added, we only need to update the factory, not the payment service. This makes the application more flexible and easier to extend in the future.