Decompose and Consolidate Conditional
What This Concept Is
Two complementary moves for single-site conditional logic. They are the go-to before anyone reaches for polymorphism.
Decompose Conditional: extract the condition, the then-branch, and the else-branch each into named functions. The code now reads if (reason()) thenAction(); else elseAction();. Conditional length goes down; intent goes up.
Consolidate Conditional Expression: when several separate conditional checks all return the same answer, combine them into one condition (with && / ||), and often extract that combined condition into a named function.
Connection to primary concept (Replace Conditional with Polymorphism): these are alternative structural moves, not mandatory prerequisites. Choose based on contract, clarity and the expected direction of change.
Why It Matters Here
A tangled multi-line if condition or a tall stack of guard-style ifs hides the why behind the what. The goal of these two refactors is to name the why.
They are also the best defense against the mistake of over-applying polymorphism. Most conditionals are not recurring and do not need a hierarchy; they need extraction and a better name.
Concrete Example
Decompose Conditional:
// Before
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd))
charge = quantity * plan.summerRate;
else
charge = quantity * plan.regularRate + plan.regularServiceCharge;
// After
if (summer(aDate, plan)) charge = summerCharge(quantity, plan);
else charge = regularCharge(quantity, plan);
function summer(aDate, plan) {
return !aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd);
}
function summerCharge(q, plan) { return q * plan.summerRate; }
function regularCharge(q, plan) { return q * plan.regularRate + plan.regularServiceCharge; }
The conditional now reads like a sentence: "if summer, charge summer; else charge regular."
Consolidate Conditional Expression:
// Before
function disabilityAmount(anEmployee) {
if (anEmployee.seniority < 2) return 0;
if (anEmployee.monthsDisabled > 12) return 0;
if (anEmployee.isPartTime) return 0;
// compute the disability amount
}
// After
function disabilityAmount(anEmployee) {
if (isNotEligibleForDisability(anEmployee)) return 0;
// compute the disability amount
}
function isNotEligibleForDisability(e) {
return e.seniority < 2
|| e.monthsDisabled > 12
|| e.isPartTime;
}
Three "same-result" guards have collapsed into one named question.
Common Confusion / Misconception
Consolidating checks that have side effects. Combining adjacent same-result guards with left-to-right short-circuit OR can preserve both order and effects. Reordering, eager argument evaluation, bitwise operators or moving intervening statements can break that equivalence. Fowler recommends pure predicates as a conservative workflow; purity is sufficient for easier reasoning, not the only possible safe case.
Don't consolidate when checks are genuinely independent. If each of the three guards reflects a different business rule that might evolve separately, leaving them as a sequence is clearer, even if repetitive.
Decomposing trivially simple conditions. if (x > 0) does not need a isPositive() helper. Decompose Conditional pays when the condition is itself multi-part or reads ambiguously.
How To Use It
Mechanics (Consolidate):
- Write the evaluation trace. Prefer pure predicates where possible, but preserve existing effect order and short-circuiting; changing effects is a separate operation.
- Combine two conditions with
||(sequence of guards returning same value) or&&(nested ifs). - Repeat until one condition remains.
- Extract the combined condition into a named function.
Check Yourself
- Why is Consolidate Conditional Expression dangerous if a condition has a side effect?
- Why are these alternatives rather than mandatory prerequisites for polymorphism?
- Give an example where leaving three separate
ifguards is preferable to consolidating them.
Mini Drill or Application
Find a function in your code with an if whose condition is at least two logical operators deep. Decompose it: name the condition, name each branch. Reread. If the function now reads like English, you are done. If not, the names are wrong -- try again or extract further.
Engineering Extension: Trace short-circuit behavior
If A returns true, the sequence if A return 0; if B return 0 never calls B. A || B in the same language can preserve that trace. helper(A(),B()) evaluates both arguments first in eager languages and cannot. The eligible lab makes this difference observable with recording and failing predicates.
Use the behavior contracts, labs and practice feedback to test the claim.
Read This Only If Stuck
Locate this move through the Refactoring book index. The selective reading guide gives exact consulted local locators and primary documentation. Index links are not exact chapter links.