Skip to content
aipatterns.dev

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

TypeScript
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

TypeScript
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

  1. Extract the whole condition into a function that returns boolean. Name it for the question, not the mechanics (isPastGracePeriod, not checkDateAndStatus). Run the tests.
  2. Extract the then branch into its own function with Extract Method. Run the tests.
  3. Extract the else branch if it is more than a trivial value. Run the tests.
  4. If both branches now return a value, collapse the if into 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 named cartIsEmpty adds 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 call b() when it was previously skipped.
  • If the branches pick between whole types of behavior and the same if recurs 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.

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

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.