DEV Community

Cover image for Don’t use if-else blocks anymore! Use Strategy and Factory Pattern Together
Tamer Ardal
Tamer Ardal

Posted on

Don’t use if-else blocks anymore! Use Strategy and Factory Pattern Together

As we move forward in a project, lost in if-else blocks, struggling with complex conditions and repetitive code, we look for a solution. But why should we be stuck in if-else blocks? In this article, let’s discover the way to get rid of if-else confusion together with Strategy and Factory patterns.

Problem: If-Else Confusion

Let’s say you are developing an e-commerce application and you need to support different payment methods like credit card, debit card and cryptocurrency. You start with if-else blocks to process payments:

public class PaymentService {

    public void processPayment(String paymentType) {
        if (paymentType.equals("CREDIT_CARD")) {
            System.out.println("Processing credit card payment...");
        } else if (paymentType.equals("DEBIT_CARD")) {
            System.out.println("Processing debit card payment...");
        } else if (paymentType.equals("CRYPTO")) {
            System.out.println("Processing crypto payment...");
        } else {
            throw new IllegalArgumentException("Invalid payment type");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

While it may seem simple at first, as payment methods increase, so will the if-else complexity. A new payment method means adding a new condition. The result is a pile of code that is difficult to manage. And this method is contrary to the Open-Closed Principle.

But, we can use the both Strategy and Factory patterns to solve this problem.

First, let’s create an enum:

public enum PaymentType {
    CREDIT_CARD,
    DEBIT_CARD,
    CRYPTO
}
Enter fullscreen mode Exit fullscreen mode

Solution: Cleaning with Strategy Pattern

public interface PaymentStrategy {
    void pay(PaymentRequest request);
}

public class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(PaymentRequest request) {
        System.out.println("Processing $type payment".replace("$type", String.valueOf(request.getPaymentType())));
    }
}

public class DebitCardPayment implements PaymentStrategy {
    @Override
    public void pay(PaymentRequest request) {
        System.out.println("Processing $type payment".replace("$type", String.valueOf(request.getPaymentType())));
    }
}

public class CryptoPayment implements PaymentStrategy {
    @Override
    public void pay(PaymentRequest request) {
        System.out.println("Processing $type payment".replace("$type", String.valueOf(request.getPaymentType())));
    }
}
Enter fullscreen mode Exit fullscreen mode

At this stage, a separate strategy for each payment method is implemented from a common interface. Now, with Factory Pattern, we will decide which strategy to choose.

Step 2: Choosing Strategy with Factory Pattern

In this step, we can make the Factory Pattern cleaner and optimized with EnumMap.

public class PaymentFactory {
    private static final Map<PaymentType, PaymentStrategy> strategies = new EnumMap<>(PaymentType.class);

    static {
        strategies.put(PaymentType.CREDIT_CARD, new CreditCardPayment());
        strategies.put(PaymentType.DEBIT_CARD, new DebitCardPayment());
        strategies.put(PaymentType.CRYPTO, new CryptoPayment());
    }

    public static PaymentStrategy getPaymentStrategy(PaymentType paymentType) {
        PaymentStrategy strategy = strategies.get(paymentType);

        if (Objects.isNull(strategy))
            throw new IllegalArgumentException("Strategy not found");

        return strategy;
    }
}
Enter fullscreen mode Exit fullscreen mode

Last Step: Service Class Reorganization

Now, let’s use what we have done.

public class PaymentService {

    public void processPayment(PaymentRequest request) {
        // Don't forget to check objects if null!
        if (Objects.isNull(request) || Objects.isNull(request.getPaymentType())
            throw new IllegalArgumentException("Request can not be null!");
        PaymentStrategy strategy = PaymentFactory.getPaymentStrategy(request.getPaymentType());

        strategy.pay(request);
    }
}
Enter fullscreen mode Exit fullscreen mode

As it is, we don’t need any if-else blocks for payment processing. Thanks to Strategy and Factory Patterns, our code is cleaner, modular and extensible.

Why Should We Use This Patterns?

1. Extensibility: Adding a new payment method only requires a new class and a few lines of code.
2. Readability: By using strategies and factory instead of if-else blocks, you make your code more understandable and manageable.
3. Maintainability: With the strategy and factory pattern, changes to the code can be made without affecting other pieces of code.

Conclusion: From Confusion to Clarity

If you are working on a growing project, you shouldn’t use if-else blocks. Strategy and Factory patterns are perfect solutions to make your code cleaner, modular and maintainable.

As you can see in this article, using design patterns instead of if-else blocks to manage payment transactions makes the project more developable and improves the readability of the code. Try these patterns in your next project instead of using if-else blocks.

...

Thank you for reading my article! If you have any questions, feedback or thoughts you would like to share, I would love to hear them in the comments.

You can follow me on dev.to for more information on this topic and my other posts.

Thank you!👨‍💻🚀

To follow me on LinkedIn: https://www.linkedin.com/in/tamerardal/
Medium: Don’t use if-else blocks anymore! Use Strategy and Factory Pattern Together

Top comments (8)

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

Great article! Your explanation of how to replace if-else blocks with the Strategy and Factory patterns is clear and practical. The use of enums and EnumMap for organizing payment strategies is a clever approach that enhances both maintainability and readability. It's a perfect example of applying design patterns to create a more scalable solution in software development. I'm definitely inspired to implement this in my own projects!

Suggestion:
Consider adding a section that discusses potential pitfalls or challenges when implementing these patterns. For instance, it could be helpful to address how to manage dependencies between payment strategies or how to handle exceptions in a way that maintains clarity and avoids overly complex error handling.

Collapse
 
tamerardal profile image
Tamer Ardal

Thanks for your suggestion 🙂

Collapse
 
blafasel3 profile image
Maximilian Lenhardt

I like this pattern, much more concise.

I would like to suggest a little improvement: I would use a switch Statement on the payment type instead of the map inside the PaymentFactory. Then you just get a compile error because a case is missing here if you happen to add another payment type. The map will fail at runtime :)

Collapse
 
lexlohr profile image
Alex Lohr

I know this pattern under the name of branchless programming - the factory pattern is usually an OOP pattern where you define objects through methods, ie. new MyProduct().color(Colors.Blue).size(Size.XL).build().

Collapse
 
schmoris profile image
Boris

That sounds like a version of the builder pattern to me, might be mistaken tho.

Collapse
 
nsadisha profile image
Sadisha Nimsara

Great article. Clearly explained how we can make use of these concepts. Appreciate it.

Collapse
 
tamerardal profile image
Tamer Ardal

Thanks 🙂

Collapse
 
gregorygaines profile image
Gregory Gaines

I wouldn't say never use if/else. What if inside credit card payments you have chip, tap, swipe, Google Pay, Apple Pay.

Now we have ChipCardPayment, TapCardPayment, SwipeCardPayment, GooglePayCardPayment, ApplePayCardPayment.

If you don't use if/else your're stuck creating a class for every condition. What about non oop languages, they won't be able to fully follow this principle.