Refactor Enterprise Code with Strategy & Factory: Two Tricks to Eliminate Endless if‑else
Learn how to replace sprawling if‑else logic in payment gateways and SaaS platforms by combining the Strategy and Factory patterns, with concrete Spring Boot examples, step‑by‑step code, and a progression from simple to abstract factories that enforce the Open‑Closed principle.
Why if‑else hell hurts your code
Many developers start a payment gateway with a method that checks the order and then routes to a specific payment channel using a cascade of if‑else statements. The example below shows a typical implementation that mixes public validation logic with channel‑specific calls for WeChat, Alipay, UnionPay, and throws an exception for unsupported types.
public PayResult pay(Order order, String payType) {
// 1. Validate order (common logic)
checkOrder(order);
// 2. Route to concrete payment logic (if‑else hell)
if ("WECHAT".equals(payType)) {
// call WeChat API, XML signing...
return wechatPay(order);
} else if ("ALIPAY".equals(payType)) {
// call Alipay API, RSA signing...
return aliPay(order);
} else if ("UNIONPAY".equals(payType)) {
// UnionPay logic...
} else {
throw new UnsupportedOperationException("Unsupported payment channel");
}
}The fatal problem is that any new channel forces a change to this core class, violating the Open‑Closed Principle (OCP): software should be open for extension but closed for modification.
First trick: Strategy pattern – polymorphic behavior replacement
Core idea : encapsulate each algorithm (strategy) in its own class so that they can be swapped without the caller knowing the concrete implementation.
1. Define the strategy interface
public interface PaymentStrategy {
PayResult pay(Order order); // execute payment
String getChannel(); // identify supported channel for the factory
}2. Implement concrete strategies
Each payment method implements the interface and contains only its own logic.
@Component
public class WechatPayStrategy implements PaymentStrategy {
@Override
public String getChannel() { return "WECHAT"; }
@Override
public PayResult pay(Order order) {
System.out.println("[WeChat] Connect gateway, XML signing...");
return new PayResult(true, "WeChat payment succeeded");
}
}
@Component
public class AliPayStrategy implements PaymentStrategy {
@Override
public String getChannel() { return "ALIPAY"; }
@Override
public PayResult pay(Order order) {
System.out.println("[Alipay] Connect gateway, RSA2 signing...");
return new PayResult(true, "Alipay payment succeeded");
}
}Adding a new channel now only requires a new class; the existing code stays untouched.
Second trick: Factory pattern – evolution of object creation
Even with strategies, the client still needs a way to obtain the correct instance. The article walks through three factory variants, showing their trade‑offs.
Simple Factory : a single factory with an if‑else that decides which concrete class to instantiate. Simple but still requires modification when a new product is added.
Factory Method : each product gets its own dedicated factory, satisfying OCP but causing a proliferation of factory classes.
Abstract Factory : a factory that creates a whole family of related objects (e.g., OSS client and CDN client for the same cloud provider). Guarantees that components from the same family are used together, at the cost of higher complexity.
Simple Factory example
public class SimpleSenderFactory {
public static MessageSender createSender(String type) {
if ("SMS".equals(type)) return new SmsSender();
else if ("EMAIL".equals(type)) return new EmailSender();
// ...
}
}The problem is obvious: adding a new sender still means editing this if‑else.
Abstract Factory for cloud providers
Define a family‑producing interface and concrete factories for each provider.
public interface CloudProviderFactory {
OssClient createOssClient();
CdnClient createCdnClient();
}
public class AliyunProviderFactory implements CloudProviderFactory {
public OssClient createOssClient() { return new AliyunOssClient(); }
public CdnClient createCdnClient() { return new AliyunCdnClient(); }
}This guarantees that an OSS client and a CDN client from the same provider are never mixed, preventing authentication failures.
Spring Boot integration – the ultimate practice
Spring’s IoC container itself acts as a powerful abstract factory. By autowiring a List<PaymentStrategy>, Spring collects all strategy beans and a @PostConstruct method builds a map for fast lookup.
@Component
public class SpringPaymentStrategyFactory {
// Spring injects all PaymentStrategy beans
@Autowired
private List<PaymentStrategy> strategyList;
private Map<String, PaymentStrategy> strategyMap;
@PostConstruct
public void init() {
strategyMap = new HashMap<>();
if (strategyList != null) {
for (PaymentStrategy strategy : strategyList) {
strategyMap.put(strategy.getChannel(), strategy);
}
}
}
public PaymentStrategy getStrategy(String channel) {
PaymentStrategy strategy = strategyMap.get(channel);
if (strategy == null) {
throw new IllegalArgumentException("Channel not configured: " + channel);
}
return strategy;
}
}The final service becomes extremely clean.
@Service
public class OrderService {
@Autowired
private SpringPaymentStrategyFactory strategyFactory;
public PayResult pay(Order order, String channel) {
checkOrder(order); // common logic
// Eliminate if‑else completely
PaymentStrategy strategy = strategyFactory.getStrategy(channel);
return strategy.pay(order);
}
}When the boss asks for "Apple Pay", you only add a new @Component that implements PaymentStrategy; the OrderService remains unchanged.
Key takeaways
Strategy pattern solves the "polymorphic algorithm replacement" problem and removes bulky if‑else chains.
Factory pattern decouples object creation from usage, so callers do not need to know concrete classes.
Spring’s List<Interface> injection plus @PostConstruct is the standard Java‑enterprise answer for combining Strategy and Factory.
Abstract factories are the ultimate tool for handling product families across platforms and preventing component mix‑ups.
Pitfall warning
Design patterns are meant for refactoring, not for premature over‑design. Start with a simple V1.0 implementation; once you have more than three if‑else branches or every new requirement forces changes to existing code, migrate to a V2.0 that applies the appropriate pattern.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tinker Programmer
Solving problems with code, sharing practical tech insights, and leveling up together!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
