如何重构级联if语句?求可行方案及适用设计模式
Hey there! I totally get being stuck on refactoring those common backend snippets from interview questions—they often look simple at first but hide messy, repetitive logic that’s tricky to clean up. Let’s break down the most frequent scenarios from that collection, along with targeted refactoring strategies and design patterns that fit each case.
Scenario 1: Repetitive Service Logic with Duplicate Error Handling/Logging
This is super common—think multiple methods that all wrap database calls in the same try-catch blocks, log identical messages, or validate parameters the same way.
Refactoring Approach
Use the Template Method Pattern to define the fixed "skeleton" of the workflow (logging, error handling) and leave variable business logic to be implemented separately. Alternatively, for cross-cutting concerns like logging/error handling, you could use decorators or aspect-oriented programming (AOP) if your framework supports it.
Example Before & After
Before (Duplicate Code):
public User getUserById(Long id) { try { log.info("Fetching user with ID: {}", id); User user = userRepository.findById(id); if (user == null) { throw new NotFoundException("User not found"); } return user; } catch (SQLException e) { log.error("Database error fetching user", e); throw new ServiceException("Failed to retrieve user"); } } public Product getProductById(Long id) { try { log.info("Fetching product with ID: {}", id); Product product = productRepository.findById(id); if (product == null) { throw new NotFoundException("Product not found"); } return product; } catch (SQLException e) { log.error("Database error fetching product", e); throw new ServiceException("Failed to retrieve product"); } }
After (Template Method):
// Base class defining the common workflow public abstract class BaseDataService { protected <T> T executeDbOperation(String operationName, Supplier<T> dbCall, Runnable notFoundCheck) { log.info("Starting {} operation", operationName); try { T result = dbCall.get(); notFoundCheck.run(); log.info("{} operation completed successfully", operationName); return result; } catch (SQLException e) { log.error("Database error during {}", operationName, e); throw new ServiceException("Failed to execute " + operationName); } } } // UserService using the template public class UserService extends BaseDataService { private UserRepository userRepository; public User getUserById(Long id) { return executeDbOperation( "fetch user ID: " + id, () -> userRepository.findById(id), () -> { User user = userRepository.findById(id); if (user == null) throw new NotFoundException("User not found"); } ); } } // ProductService follows the same pattern, no duplicate error/logic code!
Scenario 2: Bloated Conditional Branches (e.g., Handling Different Entity Types)
Another classic: a single method with endless if-else or switch blocks handling different cases (like different order types, payment methods, or data sources).
Refactoring Approach
Use the Strategy Pattern to encapsulate each branch’s logic into its own class, then use a factory to retrieve the correct strategy based on the input type. This eliminates conditionals and makes it easy to add new cases without modifying existing code (follows the Open/Closed Principle).
Example Before & After
Before (Messy Conditionals):
public void processOrder(Order order) { if (order.getType() == OrderType.PHYSICAL) { inventoryService.deductStock(order.getProductId(), order.getQuantity()); shippingService.scheduleDelivery(order); } else if (order.getType() == OrderType.DIGITAL) { digitalDeliveryService.sendDownloadLink(order.getUserId(), order.getProductId()); } else if (order.getType() == OrderType.SUBSCRIPTION) { subscriptionService.createUserSubscription(order.getUserId(), order.getProductId()); } else { throw new UnsupportedOperationException("Unsupported order type"); } }
After (Strategy Pattern):
// Strategy interface defining the common method public interface OrderProcessingStrategy { void process(Order order); OrderType getSupportedType(); } // Concrete strategy for physical orders public class PhysicalOrderStrategy implements OrderProcessingStrategy { private InventoryService inventoryService; private ShippingService shippingService; @Override public void process(Order order) { inventoryService.deductStock(order.getProductId(), order.getQuantity()); shippingService.scheduleDelivery(order); } @Override public OrderType getSupportedType() { return OrderType.PHYSICAL; } } // Repeat for DigitalOrderStrategy and SubscriptionOrderStrategy... // Factory to fetch the right strategy public class OrderStrategyFactory { private final Map<OrderType, OrderProcessingStrategy> strategyMap; // Inject all strategies via constructor (works great with dependency injection frameworks) public OrderStrategyFactory(List<OrderProcessingStrategy> strategies) { this.strategyMap = strategies.stream() .collect(Collectors.toMap(OrderProcessingStrategy::getSupportedType, Function.identity())); } public OrderProcessingStrategy getStrategy(OrderType type) { return strategyMap.getOrDefault(type, order -> { throw new UnsupportedOperationException("Unsupported order type: " + type); }); } } // Updated OrderService using the factory public class OrderService { private final OrderStrategyFactory strategyFactory; public void processOrder(Order order) { OrderProcessingStrategy strategy = strategyFactory.getStrategy(order.getType()); strategy.process(order); } }
Scenario 3: Overloaded Classes/Methods with Too Many Responsibilities
When a single class handles business logic, data validation, data conversion, and external service calls all at once—it becomes impossible to maintain or test.
Refactoring Approach
Stick to the Single Responsibility Principle (SRP) by splitting the class into focused components:
- A validator class for input validation
- A mapper class for converting between DTOs and entities
- A service class for core business logic
- Optional: A facade class if you need to expose a simplified interface to clients
Quick Example
Instead of having a UserService that validates requests, converts DTOs, and saves to the DB:
- Create
UserRequestValidatorto handle input checks - Create
UserDtoMapperto convertUserCreateDtotoUserentity - Leave
UserServiceto handle the business logic of saving the user and triggering post-save actions (like sending a welcome email)
General Refactoring Best Practices
- Write tests first: Before touching any code, ensure you have unit/integration tests that cover all cases—this prevents breaking functionality during refactoring.
- Refactor incrementally: Don’t rewrite everything at once. Make small, testable changes and validate each step.
- Prioritize readability over "clever" patterns: Don’t force a design pattern if a simple helper method will clean up the code just as well. Patterns are tools, not rules.
内容的提问来源于stack exchange,提问作者Juliatzin

