Refactoring techniqueSimplifying Conditional Expressions
Decompose Conditional
Move a complex condition and each of its branches into well-named functions so the decision reads as intent instead of arithmetic.
Problem
An if statement has a condition built from several comparisons, and each branch holds a block of calculation. You have to evaluate the boolean logic in your head to learn why the branch exists, and the branches are too long to see both at once.
Solution
Extract the condition into a function named for the question it answers, and extract each branch into a function named for what it does. The if statement stays, but it now reads as one line of intent per part.
Before
function lateFee(invoice: Invoice, today: Date): number {
let fee: number;
if (
today.getTime() > invoice.dueDate.getTime() + 3 * DAY_MS &&
invoice.status !== "disputed" &&
!invoice.customer.feeWaiver
) {
const daysLate = Math.floor((today.getTime() - invoice.dueDate.getTime()) / DAY_MS);
fee = Math.min(invoice.balance * 0.015 * Math.ceil(daysLate / 30), invoice.balance * 0.1);
} else {
fee = 0;
}
return fee;
}After
function lateFee(invoice: Invoice, today: Date): number {
return isPastGracePeriod(invoice, today) ? accruedPenalty(invoice, today) : 0;
}
function isPastGracePeriod(invoice: Invoice, today: Date): boolean {
const graceEnds = invoice.dueDate.getTime() + 3 * DAY_MS;
return today.getTime() > graceEnds && invoice.status !== "disputed" && !invoice.customer.feeWaiver;
}
function accruedPenalty(invoice: Invoice, today: Date): number {
const daysLate = Math.floor((today.getTime() - invoice.dueDate.getTime()) / DAY_MS);
const monthsLate = Math.ceil(daysLate / 30);
return Math.min(invoice.balance * 0.015 * monthsLate, invoice.balance * 0.1);
}Why
- The condition gets a name, so the reader learns the business rule without decoding three comparisons.
- Each branch can be read, tested and changed on its own.
- The top-level function shrinks to the decision, which is usually what a reviewer wants to check.
Steps
- Extract the whole condition into a function that returns
boolean. Name it for the question, not the mechanics (isPastGracePeriod, notcheckDateAndStatus). Run the tests. - Extract the
thenbranch into its own function with Extract Method. Run the tests. - Extract the
elsebranch if it is more than a trivial value. Run the tests. - If both branches now return a value, collapse the
ifinto a ternary or early return. Run the tests.
Watch out
- Do not decompose a condition that is already one clear comparison (
if (cart.isEmpty())). A wrapper namedcartIsEmptyadds a jump and no meaning. - Extracted predicates must stay side-effect free. If the original condition called something that mutates state, keep that call ordering visible.
- Watch short-circuit evaluation. Moving
a && b()into a function is safe; splitting it into two separately evaluated variables may callb()when it was previously skipped. - If the branches pick between whole types of behavior and the same
ifrecurs elsewhere, go further with Replace Conditional with Polymorphism. - There is no inverse technique; inline the functions with Inline Method if a name turns out to add nothing.
Related
- Fixes Long Method and replaces explanatory Comments with names.
- Built from Extract Method.
- Pair with Consolidate Conditional Expression when several
ifs share one outcome. - Follow with Replace Nested Conditional with Guard Clauses when decomposed branches are nested.
Agent checklist
- The condition has at least two clauses or a non-obvious meaning; otherwise leave it inline.
- Each extracted predicate is pure and named as a yes/no question.
- Short-circuit order and evaluation count of every call in the condition are unchanged.
- Tests pass after each extraction, not only at the end.
Treats these smells
See also
- Extract MethodMove a coherent fragment of a long function into its own well-named function and call it from the original place.
- Consolidate Conditional ExpressionMerge a run of separate checks that all lead to the same result into one condition with a name that states the shared rule.
- Replace Nested Conditional with Guard ClausesHandle special and invalid cases first with early returns so the normal path runs flat at the end of the function.
- Replace Conditional with PolymorphismReplace a switch on an object's type or kind with one class per case that overrides a shared method, so new cases add code instead of editing it.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.