Mastering Conditional Logic: 4 Design Patterns to Replace If‑Else Explosions
This article explores four elegant design patterns—Strategy, SPI, Chain of Responsibility, and Rule Engine—explaining their concepts, advantages, and practical Java code examples to help developers replace tangled if‑else statements with maintainable, extensible solutions.
1 Strategy Pattern
01 Concept
Strategy (Strategy Pattern) is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable, allowing the algorithm to vary independently from the client.
It replaces complex if‑else logic by delegating the decision to concrete strategy classes.
02 Example
public interface Strategy {
int doOperation(int num1, int num2);
}Concrete strategies implement the interface:
public class OperationAdd implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
public class OperationSubtract implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
public class OperationMultiply implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}The context holds a reference to a Strategy and delegates execution:
public class Context {
private Strategy strategy;
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2) {
return strategy.doOperation(num1, num2);
}
}Client code demonstrates switching strategies at runtime:
public static void main(String[] args) {
Context context = new Context();
context.setStrategy(new OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
context.setStrategy(new OperationSubtract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
context.setStrategy(new OperationMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}Advantages
Perfectly supports the Open‑Closed Principle.
Encapsulates families of algorithms.
Eliminates massive conditional statements.
Disadvantages
Clients must be aware of all concrete strategy classes.
Can lead to many small classes; the Flyweight pattern may mitigate this.
2 SPI Mechanism
01 Concept
SPI (Service Provider Interface) is a service‑discovery mechanism that loads implementations of an interface from configuration files at runtime.
Implementations are listed by fully‑qualified class name in a file under META-INF/services, and ServiceLoader reads the file to instantiate the classes.
02 Java SPI – JDBC Driver
Before JDBC 4.0, developers manually loaded the driver with Class.forName("com.mysql.jdbc.Driver"). Since JDBC 4.0, the driver is discovered via SPI, eliminating explicit loading.
// Register driver (pre‑JDBC 4.0)
Class.forName("com.mysql.jdbc.Driver");
// Open connection
Connection conn = DriverManager.getConnection(url, username, password);Internally, DriverManager uses ServiceLoader.load(Driver.class) to locate driver implementations defined in META-INF/services/java.sql.Driver.
The loading process involves:
Reading driver definitions from system properties.
Using SPI to obtain implementation class names.
Instantiating each implementation.
Selecting the driver that accepts the JDBC URL.
03 Dubbo SPI – On‑Demand Loading
Dubbo implements its own SPI (ExtensionLoader) to support on‑demand loading. Configuration files are placed under META-INF/dubbo as key‑value pairs, e.g.:
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.BumblebeeExample usage:
ExtensionLoader<Robot> loader = ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = loader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = loader.getExtension("bumblebee");
bumblebee.sayHello();Dubbo SPI adds full decoupling, hot‑plug capability, and supports IOC/AOP features.
3 Chain of Responsibility Pattern
01 Concept
The Chain of Responsibility pattern creates a linked list of handler objects, each capable of processing a request or passing it to the next handler.
It is useful when multiple objects can handle the same request and the handling order matters.
02 Example – Composite Payment
Define a handler interface:
public interface PaymentHandler {
boolean canProcess(String userId);
PaymentResult processPayment(String userId, double amount);
}Concrete handlers for corporate and personal accounts:
public class CorporatePaymentHandler implements PaymentHandler {
private static final double MAX_CORPORATE_PAYMENT = 5000;
@Override
public boolean canProcess(String userId) {
return userId.startsWith("emp_");
}
@Override
public PaymentResult processPayment(String userId, double amount) {
System.out.printf("Attempt corporate payment: user[%s], amount[%.2f]%n", userId, amount);
if (amount <= MAX_CORPORATE_PAYMENT) {
return new PaymentResult(true, amount, "Corporate payment succeeded");
} else {
return new PaymentResult(true, MAX_CORPORATE_PAYMENT,
String.format("Corporate partial payment succeeded (%.2f), remaining %.2f", MAX_CORPORATE_PAYMENT, amount - MAX_CORPORATE_PAYMENT));
}
}
}
public class PersonalPaymentHandler implements PaymentHandler {
private static final double MAX_PERSONAL_PAYMENT = 1000;
@Override
public boolean canProcess(String userId) {
return true; // all users can use personal account
}
@Override
public PaymentResult processPayment(String userId, double amount) {
System.out.printf("Attempt personal payment: user[%s], amount[%.2f]%n", userId, amount);
if (amount <= MAX_PERSONAL_PAYMENT) {
return new PaymentResult(true, amount, "Personal payment succeeded");
} else {
return new PaymentResult(true, MAX_PERSONAL_PAYMENT,
String.format("Personal partial payment succeeded (%.2f), remaining %.2f", MAX_PERSONAL_PAYMENT, amount - MAX_PERSONAL_PAYMENT));
}
}
}Composite context that iterates over handlers:
public class CompositePaymentContext {
private final List<PaymentHandler> handlers = new ArrayList<>();
public void addHandler(PaymentHandler handler) { handlers.add(handler); }
public Map<String, PaymentResult> executePayment(String userId, double totalAmount) {
Map<String, PaymentResult> results = new LinkedHashMap<>();
double remaining = totalAmount;
for (PaymentHandler handler : handlers) {
if (handler.canProcess(userId) && remaining > 0) {
PaymentResult result = handler.processPayment(userId, remaining);
results.put(handler.getClass().getSimpleName(), result);
if (result.isSuccess()) {
remaining -= result.getPaidAmount();
if (remaining <= 0) break;
}
}
}
if (remaining > 0) {
results.put("Remaining", new PaymentResult(false, remaining,
String.format("Payment incomplete, remaining amount: %.2f", remaining)));
}
return results;
}
}Test class demonstrates three scenarios: corporate‑only payment, composite payment, and personal‑only payment.
4 Rule Engine
01 Concept
Rule engines separate business rules from code, allowing non‑technical users to configure rules that are stored as strings (e.g., in a database) and evaluated at runtime.
02 Example – AviatorScript
Rule script for a discount activity:
if (amount >= 1000) {
return 200;
} elsif (amount >= 500) {
return 100;
} else {
return 0;
}Utility method to evaluate the script:
public static BigDecimal getDiscount(BigDecimal amount, String rule) {
Map<String, Object> env = new HashMap<>();
env.put("amount", amount);
Expression expression = AviatorEvaluator.compile(DigestUtils.md5Hex(rule.getBytes()), rule, true);
Object result = expression.execute(env);
if (result != null) {
return new BigDecimal(String.valueOf(result));
}
return null;
}Test execution:
String rule = "if (amount>=1000){
return 200;
}elsif(amount>=500){
return 100;
}else{
return 0;
}";
BigDecimal discount = getDiscount(new BigDecimal("600"), rule);
System.out.println("discount:" + discount); // prints 100Advantages
Business personnel can configure rules without developer involvement.
Reduces development and deployment cost.
Improves rule transparency and maintainability.
5 Summary
In everyday development, conditional‑explosion problems arise when many if/else branches are needed. Traditional nested if/else code becomes hard to read, maintain, and extend.
The article presented four design patterns—Strategy, SPI, Chain of Responsibility, and Rule Engine—that address this issue by encapsulating varying behavior, enabling runtime discovery, chaining handlers, and externalizing business rules.
Choosing the right pattern follows core design principles such as Single Responsibility, Open‑Closed, high cohesion & low coupling, and KISS, ultimately leading to maintainable, extensible, and well‑structured code.
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.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
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.
