來繼續我們的重構之旅吧!
在上一章中我們把折扣的部份抽出來做為一個函式:
function discount(cartTotal) {
    if (cartTotal >= 500) {
      //修改全域變數並指派回去
      cartTotal = cartTotal * 0.8
    } else if (cartTotal >= 300) {
      //跟上面一樣,只是乘的數字不同
      cartTotal = cartTotal * 0.85
    } else if (cartTotal >= 100){
      //跟上面一樣
      cartTotal = cartTotal * 0.9
    }
    return cartTotal;
}
// 原先函式中計價總價的部份
cartTotal = discount(carTotal)
首先我們避免在這個函式改動外部傳進來的 cartTotal, 而是單純回傳打折的折數。
// 改個好一點的名字
function getDiscountRate(cartTotal) {
    if (cartTotal >= 500) {
      return 0.8
    } else if (cartTotal >= 300) {
      return 0.85
    } else if (cartTotal >= 100){
      return 0.9
    } else {
      return 1
    }
}
// 當然,原先函式中計價總價的部份也要跟著修改,變成乘以折扣數
cartTotal = cartTotal * getDiscountRate(carTotal)
但是如果以後要新增或是修改折扣的數字,就要慢慢的找到是哪一條要改,也可能要加入多個新的 else if 條件區塊。這種寫法把規則和程式碼混在一起,像是老師每次改考卷都要記得:90分以上是優等、80分以上是良好...如果教育部說要改標準,老師要重新記住新的規則。
比較好的寫法是「學校給老師一張『評分對照表』,老師只要會查表就好。要改標準時,只要換一張新的表就行了,老師的改考卷方法不用變。」我們把規則的部份改成用一個陣列存目前的折扣,而程式碼就是單純的由上往下找,找到符合的條件就回傳。
// 規則的部份
// 用大寫告訴大家這個變數不能用程式變動,只能用人工改
// 之後要新的折扣條件在這裡加就好
const DISCOUNT_TIERS = [
  {minAmount: 500, rate: 0.8},
  {minAmount: 300, rate: 0.85},
  {minAmount: 100, rate: 0.9},
  {minAmount: 0, rate: 1},
]
// 程式碼邏輯的部份
function getDiscountRate(cartTotal) {
    //有奇怪的東西(如負數)傳進來時的防禦
    if(!Number.isInteger(cartTotal) || cartTotal < 1) { return 1.0; }
	// 從上面往下找,找到了就回傳
    for (let tier of DISCOUNT_TIERS) {
      if (cartTotal >= tier.minAmount) {
          return tier.rate;
      }
    }
}
// 原先函式中計價總價的部份一樣是乘以折扣數
cartTotal = cartTotal * getDiscountRate(carTotal)
來看看加總函式裡迴圈的部份:
// 【魔術數字】3 應該用 amounts.length
for (var i = 0; i < 3; i++) {
  // 厲害的解構賦值招式
  let {name, amount} = amounts[i];
  // 因為用了字典,所以可以直接用 name 當 key 來取得價格
  cartTotal = itemPrice[name] * amount;
}
在這裡我們要考慮的是,我們寫出來的條件,有包含了所有可能的情況嗎?程式界有個思考的準則,叫 MECE (Mutually Exclusive, Collectively Exhaustive),中文翻譯是「相互獨立,完全窮盡」。
Mutually Exclusive(相互獨立) 是說
Collectively Exhaustive (完全窮盡) 是說
我們的迴圈程式碼有幾個問題
itemPrice 裡不存在的項目會壞掉我們再把找到一個玩具的價格,乘上數量這段抽出去,變成一個函式
// 上面的商品價格字典
let itemPrice = {
    "遙控車": 250,
    "玩偶": 180,
    "拼圖": 120
}
// 我們的購物車
let cart = [
    {name: "遙控車", amount: 2}, 
    {name: "玩偶", amount: 0}, 
    {name: "拼圖", amount: -10}
]
function calc(cart) {
    var cartTotal = 0;
    
    console.log("開始計算購物車總金額...");
    // 改用 carts.length
    // 一般來說會避免單字母的變數名稱,但迴圈用 i 可以接受
    for (var i = 0; i < carts.length; i++) {
      // 如果有奇怪的東西,金額會被加 0,所以不影響程式運作
      cartTotal += getItemTotal(carts[i])
    }
    
    // 打折的計算
    cartTotal = cartTotal * discount(cartTotal);
    // 【副作用】印出結果
    console.log("購物車總金額:" + cartTotal + " 元");
    console.log("========================");
    return cartTotal
}
function getItemTotal(item) {
  // 厲害的解構賦值招式
  let {name, amount} = item;
  //防禦負數的數量,或是取不到對應的商品價格
  if (amount < 1 || !itemPrice[name]) { return 0; }
  
  return itemPrice[name] * amount
}
最後不要在函裡裡列印,直接回傳計算的數值,我們就把整段函式 refactoring 成這個樣子了:
// 商品價格字典, 以後可以很方便的新增或修改商品跟價格
let itemPrice = {
  "遙控車": 250,
  "玩偶": 180,
  "拼圖": 120
}
// 改成用物件,可以用 name 當 key 來取得 itemPrice 的價格
let cart = [
  {name: "遙控車", amount: 2}, 
  {name: "玩偶", amount: 0}, 
  {name: "拼圖", amount: -10}
]
// 用大寫告訴大家這個變數不能用程式變動,只能用人工改
// 之後要新的折扣條件在這裡加就好
const DISCOUNT_TIERS = [
  { minAmount: 500, rate: 0.8 },
  { minAmount: 300, rate: 0.85 },
  { minAmount: 100, rate: 0.9 },
  { minAmount: 0,   rate: 1.0 }
];
// 核心計算程式
function calc(cart) {
    var cartTotal = 0;
    for (var i = 0; i < cart.length ; i++) {
      cartTotal += getItemTotal(carts[i])
    }
    
    // 直接回傳打折後的結果
    return cartTotal * getDiscountRate(cartTotal);
}
//取得單項商品的總價
function getItemTotal(item) {
  // 厲害的解構賦值招式
  let {name, amount} = item;
  //防禦負數的數量,或是取不到對應的商品價格
  if (amount < 1 || !itemPrice[name]) { return 0; }
  
  return itemPrice[name] * amount
}
//取得折扣的費率
function getDiscountRate(cartTotal) {
  for (let tier of DISCOUNT_TIERS) {
    if(!Number.isInteger(cartTotal) || cartTotal < 1) { return 1.0; }
    
    for (let tier of DISCOUNT_TIERS) {
      if (cartTotal >= tier.minAmount) {
        return tier.rate;
      }
    }
  }
}
let total = calc(amounts);
console.log("目前總金額:" + total + " 元");
跟之前的樣子比對一下,你有沒有覺得乾淨而且漂亮很多呢?
// ========== 有很多壞味道的購物車程式碼 ==========
// 【全域變數】所有資料都是全域的,容易被意外修改
var cartTotal = 0;
var itemPrice = [
  {name: "遙控車", price: 250}, 
  {name: "玩偶", price: 180}, 
  {name: "拼圖", price: 120}
];  // 商品價格
var amounts = [2, 0, -10]; // 購買數量
function calc() {
    // 【副作用】直接修改全域變數
    cartTotal = 0;
    
    console.log("開始計算購物車總金額...");
    
    // 【魔術數字】3 應該用 amounts.length
    for (var i = 0; i < 3; i++) {
        // 【命名不清】t 是什麼意思?
        var t = itemPrice[i].price * amounts[i];
        
        // 【副作用】修改全域變數
        cartTotal = cartTotal + t;
    }
    
    // 打折的計算
    if (cartTotal >= 500) {
      //修改全域變數並指派回去
      cartTotal = cartTotal * 0.8
    } else if (cartTotal >= 300) {
      //跟上面一樣,只是乘的數字不同
      cartTotal = cartTotal * 0.85
    } else if (cartTotal >= 100){
      //跟上面一樣
      cartTotal = cartTotal * 0.9
    }
    
    // 【副作用】印出結果
    console.log("購物車總金額:" + cartTotal + " 元");
    console.log("========================");
}
// 執行計算
calc();
我們把程式邏輯從約 30 行降到 22 行,比原本的程式碼還短。不但防止了很多原先會出錯的部份,而且變得更好懂。這就是程式碼品質的重要之處。
我們完成了把一大段程式碼重構的過程。用的手法包括這些:
另外我們還學到 MECE 「相互獨立,完全窮盡」這個思考的準則。
