在上一章中我們把折扣的部份抽出來做為一個函式:
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(相互獨立) 是說
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 += 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 「相互獨立,完全窮盡」這個思考的準則。