Reconstruction technique -- simplifying conditional logic

Posted by islan on Thu, 10 Feb 2022 09:08:31 +0100

Most of the functions of the program come from conditional logic, and most of the complexity comes from conditional logic. Sometimes we need to use refactoring to make conditional logic easier to understand. For example:

  • Explode conditional expressions: handle complex conditional expressions
  • Merge condition expression: clarify logical combination
  • Replace nested conditional expressions with guard statements: check before processing the main logic
  • switch logic: replacing conditional expressions with polymorphisms

Decompose Conditional expression

This refactoring technique is actually just an application scenario for refining functions. We can use refining functions for conditional judgment and each conditional branch

// Assuming that the total price of a certain commodity is calculated (total price = quantity x unit price), the unit price of the commodity is different in winter and summer:
if (!aData.isBefore(plan.summerStart) && !aData.isAfter(plan.summerEnd)) {
  charge = quantity * plan.summerRate;
} else {
  charge = quantity * plan.regularRate + plan.regularServiceCharge;
}
// After refining:
// if (summer()) {
//   charge = summerCharge();
// } else {
//   charge = regularCharge();
// }
charge = summer() ? summerCharge() : regularCharge();

function summer() {
  return !aData.isBefore(plan.summerStart) && !aData.isAfter(plan.summerEnd);
}
function summerCharge() {
  return quantity * plan.summerRate;
}
function regularCharge() {
  return quantity * plan.regularRate + plan.regularServiceCharge;
}

Consolidate Conditional Expression

Practice:

  • Make sure these conditional expressions have no side effects
  • Combine two related conditional expressions into one using appropriate logical operators
  • Repeat the previous merge process until all related conditional expressions are merged together
function disabilityAmount(anEmployee) {
  if (anEmployee.seniority < 2) return 0;
  if (anEmployee.monthsDisabled > 12) return 0;
  if (anEmployee.isPartTime) return 0;
  // ...
}

// Refining function after merging
function disabilityAmount(anEmployee) {
  if (isNotEligableForDisability()) return 0;
  // ...
}
function isNotEligableForDisability() {
  return ((anEmployee.seniority < 2) 
          || (anEmployee.monthsDisabled > 12) 
          || (anEmployee.isPartTime));
}

Replace Nested Conditional With Guard Clauses

Conditional expressions usually have two styles: both conditional branches belong to normal behavior; Only one conditional branch is normal behavior, and the other branch is abnormal. In the second case, if a condition is extremely rare, it should be checked separately and returned from the function immediately when the condition is true. Such a separate check is usually called "guard statement".

// Example 1
function payAmount(employee) {
  let result;
  if (employee.isSeparated) {
    result = {amount: 0, reasonCode: 'SEP'};
  } else {
    if (employee.isRetired) {
      result = {amount: 0, reasonCode: 'RET'};
    } else {
      // logic to compute amount
      result = someFinalComputation();
    }
  }
  return result;
}
// After modification:
function payAmount(employee) {
  if (employee.isSeparated) return {amount: 0, reasonCode: 'SEP'};
  if (employee.isRetired) return {amount: 0, reasonCode: 'RET'};
  // logic to compute amount
  return someFinalComputation();
}

// Example 2: conditional inversion
function adjustedCapital(anInstrument) {
  let result = 0;
  if (anInstrument.capital > 0) {
    if (anInstrument.interestRate > 0 && anInstrument.duration > 0) {
      result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
    }
  }
  return result;
}
// After modification:
function adjustedCapital(anInstrument) {
  if (   anInstrument.capital      <= 0 
      || anInstrument.interestRate <= 0 
      || anInstrument.duration     <= 0) {
    return 0;
  }
  return (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;;
}

Replace Conditional With Polymorphism

Complex conditional logic is one of the most difficult things to understand in programming. Sometimes we can split conditional logic into different scenarios to disassemble complex conditional logic. Sometimes the structure of conditional logic itself is enough to express this kind of splitting, but the use of classes and polymorphisms can express the splitting of logic more clearly. One of the key features of object-oriented programming is that it should not be easily replaced by polymorphism.

// Example: there is a flock of birds, which are characterized by how fast they are and what their feathers are like
function plumages(birds) {
  return new Map(birds.map(b => [b.name, plumage(b)]));
}
function speeds(birds) {
  return new Map(birds.map(b => [b.name, airSpeedVelocity(b)]));
}
function plumage(bird) {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return 'average';
    case 'AfricanSwallow':
      return (bird.numberOfCoconuts > 2) ? 'tired' : 'average';
    case 'NorwegianBlueParrot':
      return (bird.voltage > 100) ? 'scorched' : 'beautiful';
    default:
      return 'unknown';
  }
}
function airSpeedVelocity(bird) {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return 35;
    case 'AfricanSwallow':
      return 40 - 2 * bird.numberOfCoconuts;
    case 'NorwegianBlueParrot':
      return (bird.isNailed) ? 0 : bird.voltage / 10;
    default:
      return null;
  }
}
// After refactoring
function plumages(birds) {
  return new Map(birds
                .map(b => createBird(b))
                .map(bird => [bird.name, bird.plumage]));
}
function speeds(birds) {
  return new Map(birds
                .map(b => createBird(b))
                .map(bird => [bird.name, bird.airSpeedVelocity]));
}

function createBird(bird) {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return new EuropeanSwallow(bird);
    case 'AfricanSwallow':
      return new AfricanSwallow(bird);
    case 'NorwegianBlueParrot':
      return new NorwegianBlueParrot(bird);
    default:
      return new Bird(bird);
  }
}

class Bird {
  constructor(birdObject) {
    Object.assign(this, birdObject);
  }
  get plumage() {
    return 'unknown';
  }
  get airSpeedVelocity() {
    return null;
  }
}
class EuropeanSwallow extends Bird {
  get plumage() {
    return 'average';
  }
  get airSpeedVelocity() {
    return 35;
  }
}
class AfricanSwallow extends Bird {
  get plumage() {
    return (bird.numberOfCoconuts > 2) ? 'tired' : 'average';
  }
  get airSpeedVelocity() {
    return 40 - 2 * bird.numberOfCoconuts;
  }
}
class NorwegianBlueParrot extends Bird {
  get plumage() {
    return (bird.voltage > 100) ? 'scorched' : 'beautiful';
  }
  get airSpeedVelocity() {
    return (bird.isNailed) ? 0 : bird.voltage / 10;
  }
}

Topics: Javascript