iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0

打折的函式

在上一章中我們把折扣的部份抽出來做為一個函式:

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 條件區塊。這種寫法把規則和程式碼混在一起,像是把菜單直接寫在廚師的腦子裡。如果要改菜單,就要重新訓練廚師。

比較好的寫法是「把規則寫在一張表上,廚師只要會看表就好。要改菜單,只要改那張表,不用改廚師的做菜方法。」我們把規則的部份改成用一個陣列存目前的折扣,而程式碼就是單純的由上往下找,找到符合的條件就回傳。

// 規則的部份
// 用大寫告訴大家這個變數不能用程式變動,只能用人工改
// 之後要新的折扣條件在這裡加就好
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(相互獨立) 是說

  • 每個分類之間沒有重疊
  • 一個東西只能屬於一個分類
  • 不會有「既是A又是B」的情況

Collectively Exhaustive (完全窮盡) 是說

  • 所有可能的情況都有被考慮到
  • 沒有遺漏
  • 不會有「不屬於任何分類」的情況。

我們的迴圈程式碼有幾個問題

  1. 傳進來的購物車超過 3 個的時候,後面的不會被計算到
  2. 沒有檢查金額
  3. 如果 "name" 被設成 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 += getPrice(carts[i])
  }
    
  // 直接回傳打折後的結果
  return cartTotal * getDiscountRate(cartTotal);
}

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;
    }
  }
}

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 行降到 25 行,比原本的程式碼還短了 1/6。不但防止了很多原先會出錯的部份,而且變得更好懂。這就是程式碼品質的重要之處。

營火前的回顧

我們完成了把一大段程式碼重構的過程。用的手法包括這些:

  • 避免修改/使用全域變數
  • 把一件獨立的運算抽出去做為函式
  • 善用函式的參數與回傳值
  • 把條件資料跟程式運算邏輯分離
  • 防禦有問題的狀況

另外我們還學到 MECE 「相互獨立,完全窮盡」這個思考的準則。

測驗

  • 如果要幫班上同學的興趣分類,要怎麼分類可以做到 MECE?
  • 我們程式碼在打折規則的部份, 一定要由金額高的排到金額低的嗎?不好好排會怎樣?
  • 跟 AI 討論以及朋友下列的話題吧!
    • 有哪些程式碼的壞味道
    • 重構的手法有哪些
    • AI 時代,程式碼品質裡哪些會變得比較重要,哪些變得比較不重要了

地圖


上一篇
Ch 19. 程式碼也有分漂亮的跟醜的?還有味道?
系列文
Just enough code with AI: 給新手們的程式設計世界觀21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言