Introduction
In this tutorial, we will explore how to effectively refactor a long statement chain into a Map
, along with key considerations regarding readability and maintainability to ensure the code remains robust and easy to work with over time.
🚀Preparing For Java Interview? Checkout: Grokking the Java Interview. 🚀
🚀 Ready to Level Up Your Development Skills? Checkout: Get Your Hands Dirty on Clean Architecture 🚀
Question
Suppose you’re refactoring a legacy Java codebase with a long statement chain. How would you approach replacing it with a Map
? What considerations would you make regarding readability and maintainability?
Example of legacy code:
public void processPayment(String paymentMethod) {
if ("CREDIT_CARD".equals(paymentMethod)) {
processCreditCardPayment();
} else if ("PAYPAL".equals(paymentMethod)) {
processPaypalPayment();
} else if ("BANK_TRANSFER".equals(paymentMethod)) {
processBankTransferPayment();
} else {
processDefaultPayment();
}
}
Solution
In our example, the if-else chain handles different payment methods and executes the corresponding function for each method. if the number of payment methods grows, this chain can become difficult to manage and read.
Refactoring Using a Map
To refactor this code, we can use a Map to store each payment method as a key and the corresponding action as the value. This approach makes the code more maintainable and easier to understand.
Step-by-step refactoring:
Define the Mapping: Use a Map<String, Runnable> to map each payment method to its respective processing logic. Runnable is a functional interface that allows us to execute logic without parameters.
private Map<String, Runnable> paymentMethodMap = new HashMap<>();
public PaymentProcessor() {
paymentMethodMap.put("CREDIT_CARD", this::processCreditCardPayment);
paymentMethodMap.put("PAYPAL", this::processPaypalPayment);
paymentMethodMap.put("BANK_TRANSFER", this::processBankTransferPayment);
}
2. Replace the Long Statement Chain: Instead of using the if-else chain, use the map to look up the correct action based on the payment method.
public void processPayment(String paymentMethod) {
Runnable action = paymentMethodMap.getOrDefault(paymentMethod, this::processDefaultPayment);
action.run();
}
Readability Consideration
Self-Descriptive Keys: We can use descriptive strings like
"CREDIT_CARD"
,"PAYPAL"
etc., to make the code more readable. We can also use constants instead of raw strings to avoid typos and provide additional clarity.
private static final String CREDIT_CARD = "CREDIT_CARD";
private static final String PAYPAL = "PAYPAL";
private static final String BANK_TRANSFER = "BANK_TRANSFER";
paymentMethodMap.put(CREDIT_CARD, this::processCreditCardPayment);
Extract Complex Logic: If the
Runnable
logic is more complex, it’s a good idea to extract it into a method with a descriptive name to maintain readability.
4. Maintainability Considerations
Centralized Definition: The Map centralizes all payment methods and their corresponding logic, making it easy to add, modify, or remove payment methods without changing the main processing flow.
for example, if we need to add a new payment method ("APPLE_PAY"
), we can simply add:
paymentMethodMap.put("APPLE_PAY", this::processApplePayPayment);
Avoid Duplicate Code: This approach eliminates the repetitive
if-else
code, making changes more straightforward and reducing the likelihood of errors.
5. Example Refactored Code Summary
public class PaymentProcessor {
private static final String CREDIT_CARD = "CREDIT_CARD";
private static final String PAYPAL = "PAYPAL";
private static final String BANK_TRANSFER = "BANK_TRANSFER";
private Map<String, Runnable> paymentMethodMap = new HashMap<>();
public PaymentProcessor() {
paymentMethodMap.put(CREDIT_CARD, this::processCreditCardPayment);
paymentMethodMap.put(PAYPAL, this::processPaypalPayment);
paymentMethodMap.put(BANK_TRANSFER, this::processBankTransferPayment);
}
public void processPayment(String paymentMethod) {
Runnable action = paymentMethodMap.getOrDefault(paymentMethod, this::processDefaultPayment);
action.run();
}
private void processCreditCardPayment() {
System.out.println("Processing credit card payment...");
}
private void processPaypalPayment() {
System.out.println("Processing PayPal payment...");
}
private void processBankTransferPayment() {
System.out.println("Processing bank transfer payment...");
}
private void processDefaultPayment() {
System.out.println("Processing default payment...");
}
}
Conclusion
Using a Map approach is a great way to make the codebase more extensible, concise, and easier to understand, especially when dealing with multiple options that map naturally to key-value pairs.
I seriously wonder if Java developers ever think about the performance of their code. As a c++ developer it amazes me what you have suggested here is so brutal to the cpu branch prediction and cache coherency that it will no doubt kill what ever the poor jvm can optimize. Clean readable code is useless if it doesn’t perform. If you disagree, show me JMH benchmarks.