Try   HackMD

重構 - Chapter 10 簡化條件邏輯

1. 分解條件邏輯 (Decompose Conditional)

動機: 凸顯意圖、降低閱讀複雜度
作法: 將分支中的條件邏輯抽成 函式 (Extract Function)

From

if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) 
    charge = quantity * plan.summerRate; 
else
    charge = quantity * plan.regularRate + plan.regularServiceCharge;

To

if (summer())
    charge = summerCharge();
else
    charge = regularCharge();

To

const charge = summer() ? summerCharge() : regularCharge()

2. 合併條件式(Consolidate Conditional Expression)

動機: 當檢查不同、底下卻做相同的事時,代表 1. 檢查可結合、 2. 可抽出函式 (Extract Function)凸顯意圖 (封裝、隱藏細節,從說 how 變成說 why)
作法:

  1. 確保條件式中沒 Side Effect (否則請先執行 Separate Query from Modifier)
  2. 將兩個條件式中,用邏輯運算子結合 (or -> 依序、 and -> 巢狀)
  3. 測試
  4. 將結合完的這個條件式與下個條件結合,直到只剩一個條件式
  5. 考慮將此條件式抽成 Function

From

function disabilityAmount (anEmployee) {
    if (anEmployee.seniority < 2) return 0; 
    if (anEmployee .monthsDisabled > 12) return 0;
    if (anEmployee.isPartTime) return 0; 
    // 計算 disability amount
    ...
}

To

function disabilityAmount (anEmployee) {
    if (isNotEligableForDisability()) return 0;
    //計算 disability anount
    ...
    function isNotEligableForDisability() {
        return ((anEmployee.seniority < 2)
                || (anEmployee.monthsDisabled > 12)
                || (anEmployee.isPartTime));
    }
}

From

if(anEmployee.onVacation)
    if (anEmployee.seniority > 10)
        return 1;
return 0.5;

To

if ((anEmployee.onVacation) && (anEmployee.seniority > 10)) return 1;
return 0.5;

3. 將巢狀條件式換成防衛敘句(Replace Nested Conditional with Guard Clauses)

動機: 使入口和出口更“清晰”!
防衛敘句能指出某情況非功能“核心”,發生了就採取行動離開。有意識到思考你的條件分支是屬於正常 or 異常,(2組都正常,用 if/else <= 權重相同;有一組為異常 <= 提早離開)

作法:

  1. 先將最外層需要替換的條件轉為 Guard Clauses
  2. 測試
  3. 重複該行為
  4. 最後如果 Guard Clauses 都回傳相同結果,可使用二:合併條件式

From

function getPayAmount () {
    let result;
    if (isDead)
        result = deadAmount();
    else {
        if (isSeparated)
            result = separatedAmount();
        else {
            if (isRetired)
                result = retiredAmount();
            else
                result = normalayAmount();
        }
    }
    return result;
}

To

function getPayAmount () {
    if (isDead) return deadAmount() ;
    if (isSeparated) return separatedAmount();
    if (isRetired) return retiredAmount();
    return normalPayAmount ();
}

From

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; } // 條件反轉、簡化過程 + if (anInstrument.capital <= 0) return result + if (!(anInstrument.interestRate > 0 && anInstrument.duration > 0)) return result + if (anInstrument.interestRate <= 0 || anInstrument.duration <= 0) return result + 合併 12,14+ 移除多餘的變數 result

註解:狄摩根定律 (!a) && (!b) === !(a || b)(!a) || (!b) === !(a && b)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

To


function adjustedCapital (anInstrument) {
if ( anInstrument.capital <= 0
|| anInstrument.interestRate <= 0
|| anInstrument.duration<= 0 ) return 0;
return (anInstrument.income / anInstrument .duration) * anInstrument .adjustmentFactor

4. 將條件式換成多型(Replace Conditional with Polymorphism)

5. 加入特例(Introduce Special Case)

6. 加入斷言(Introduce Assertion)

動機: 凸顯意圖、寶貴的溝通工具、除錯的好幫手
作法: 當你見到一個條件為真的狀況時,加入斷言來說明此事

From

if(this.discountRate)
    base = base - (this.discountRate * base)

To

assert(this.discountRate >= 0) // 斷言失敗代表程式碼有錯
if(this.discountRate)
    base = base - (this.discountRate * base)


// assert 大致會長得像下面這樣,nodejs 有提供 API
function assert(condition, message) {
    if (!condition) {
        throw new Error(message || "Assertion failed");
    }
}

Discucsion

  1. 盡量使用正向表述,人腦比較好理解
disabled = !isEligible || !isActive
disabled = !(isEligible && isActive)
enabled = isEligible && isActive
  1. 在單元測試、TypeScript 型別斷言都還滿常見 assert 的概念
expect(add(5, 5)).toBe(10);
let str: unknown = "geeksforgeeks";  
let len: number = (str as string).length;
  1. IIFE 變數,來替代巢狀三元運算式
  const content = (() => {
    switch (status) {
      case 'loading':
        return <LoadingBar  />;
      case 'loaded':
        return <Table />;
      default:
        return null;
    }
  })();

  return (
    <Container>
      {content}
    </Container>
  1. 用 switch case true 來做多重判斷
    https://seanbarry.dev/posts/switch-true-pattern
const user = {
	firstName: "Seán",
	lastName: "Barry",
	email: "my.address@email.com",
	number: "00447123456789",
};

switch (true) {
	case !user:
		throw new Error("User must be defined.");
	case !user.firstName:
		throw new Error("User's first name must be defined");
	case typeof user.firstName !== "string":
		throw new Error("User's first name must be a string");
	// ...lots more validation here
	default:
		return user;
}
  1. 用 object literal 取代 switch case
    https://ultimatecourses.com/blog/deprecating-the-switch-statement-for-object-literals

// from
  var drinks;
  switch(type) {
  case 'coke':
    drink = 'Coke';
    break;
  case 'pepsi':
    drink = 'Pepsi';
    break;
  default:
    drink = 'Unknown drink!';
  }

// to
function getDrink (type) {
  var drinks = {
    'coke': 'Coke',
    'pepsi': 'Pepsi',
    'lemonade': 'Lemonade',
    'default': 'Default item'
  };
  return 'The drink I chose was ' + (drinks[type] || drinks['default']);
}

參考資料

  1. 重構:改善既有程式的設計
  2. Code の 斷捨離 — 重構 (Refactoring)-ch9
  3. What is “assert” in JavaScript?