Applying the Responsibility Tree Pattern for Versioned API Refactoring
To refactor a versioned API plagued by nested guards, the article proposes the Responsibility Tree pattern—a hybrid of Chain of Responsibility and Strategy—that routes requests through hierarchical handlers based on parameters, yielding cleaner code, easier debugging, and scalable extension for future versions.
Background: A recent requirement demands an API to return results based on various combinations of parameters p1, p2, p3 and version. The existing implementation has evolved over three iterations, accumulating guard clauses that check the version and nested if‑else branches for the parameters, resulting in noticeable code smell.
The logic is a generic business capability that will likely see further versions (phase 4, 5, N) and additional parameter values, so the current approach would only increase the technical debt.
Key problems:
How to upgrade the interface while preserving compatibility with older versions?
How to map different combinations of p1, p2, p3, etc., to the appropriate business strategy?
Two classic design patterns come to mind: the Chain of Responsibility and the Strategy pattern.
Chain of Responsibility creates a pipeline‑like structure where each handler either processes the request or forwards it to the next handler. This can isolate version‑specific logic by assigning each version to a distinct node in the chain.
However, the scenario also requires routing based on specific strategies, which is where the Strategy pattern excels.
Strategy Pattern decouples the algorithm from its usage, allowing the system to select a concrete strategy at runtime based on defined rules.
Neither pattern alone fully satisfies the requirements: the chain handles sequential delegation but lacks multi‑parameter routing, while the strategy pattern typically provides only a single level of routing. Combining them yields a generalized chain, termed the Responsibility Tree pattern.
Responsibility Tree merges the hierarchical delegation of the chain with the flexible routing of strategies. Each parameter can correspond to a level in the tree, and each value routes to a child node, enabling clean extension for new parameters or values.
Benefits observed after refactoring with the Responsibility Tree:
Clearer code structure and improved maintainability (functions stay within a reasonable size).
Easier debugging because each strategy is isolated.
Reduced future iteration effort and lower risk of errors when adding new requirements.
The author abstracts the pattern into a reusable framework consisting of a Router abstract class and a Handler interface.
@Component
public abstract class AbstractStrategyRouter
{
public interface StrategyMapper
{
StrategyHandler
get(T param);
}
private StrategyMapper
strategyMapper;
@PostConstruct
private void abstractInit() {
strategyMapper = registerStrategyMapper();
Objects.requireNonNull(strategyMapper, "strategyMapper cannot be null");
}
@Getter
@Setter
@SuppressWarnings("unchecked")
private StrategyHandler
defaultStrategyHandler = StrategyHandler.DEFAULT;
public R applyStrategy(T param) {
final StrategyHandler
strategyHandler = strategyMapper.get(param);
if (strategyHandler != null) {
return strategyHandler.apply(param);
}
return defaultStrategyHandler.apply(param);
}
protected abstract StrategyMapper
registerStrategyMapper();
}Simple routing logic can be implemented with if‑else statements or a Map for better performance. Nodes that implement the router expose the public R applyStrategy(T param) method, which recursively delegates to child nodes until a leaf handler produces the final result.
/**
* @author: 寻弈
* @date: 2020/4/16 2:46 下午
*/
public interface StrategyHandler
{
@SuppressWarnings("rawtypes")
StrategyHandler DEFAULT = t -> null;
/**
* apply strategy
*
* @param param
* @return
*/
R apply(T param);
}Intermediate nodes inherit the router abstract class and implement the handler interface, performing input validation (fail‑fast) before delegating downstream. This design keeps each layer focused on its responsibility while remaining easily extensible.
Additional reflections emphasize that while design patterns provide powerful abstractions, they are not a universal replacement for simple if‑else logic. Premature optimization should be avoided, and thoughtful architectural decisions must consider context, maintainability, and future evolution.
Xianyu Technology
Official account of the Xianyu technology team
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.