你有沒有聽過軟體工程師討論時,會講到「這段程式碼有壞味道」?程式碼又不是食物,怎麼會有壞味道呢?我們在上一篇討論了軟體工程裡的「版本控制」,而工程師們討論時說的壞味道,就是軟體工程中,討論這段程式碼的品質好不好。有經驗的工程師可以在很短的時間內,看出程式碼品質的好壞,就像厲害的廚師,可以分辨出一道料理的好壞一樣。
程式碼品質在乎的點大概是下列這幾項:
當然在生成式 AI 的時代中,你可能不用看懂每一行程式碼,所以好不好懂的重要性會稍微降低,但是對於最關鍵的運作邏輯裡,是否能讓人理解還是很重要的。不然程式被別人亂操作,有可能會有不好的後果的。
例如我們做了一個賣玩具的線上商店,只賣我們自己做的「超棒玩具」,一個 100 元。商店的頁面長下面這樣。想買的人輸入他想買的數量,按下結帳,就會跟買的人收取底下「總金額」的價格。
但是有個壞人來用我們的商店,他在「購買數量」的欄位裡,輸入負數!變成我們無緣無故要退錢給他!如果沒有檢查就讓商店運作的話,我們就損失慘重了!
我們用 JavaScript 的程式碼來示範,除了上次說的函式是 function(){}
以外,JavaScript 比較習慣用駝峰字來命名變數:
# Python 變數/函式名稱用 snake_case
# 蛇形狀的字,用底線分隔
# Elixir, Ruby, PHP, Rust, Perl 語言也是會用這種
snake_case
student_name
item_price
// JavaScript 變數/函式名稱用 camelCase
// 駱駝形狀的字,第一個字小寫,後面大寫開頭
// JAVA, C#, Kotlin, Swift 語言是習慣這種
camelCase
studentName
itemPrice
不過變數的取法只是習慣,多人合作時通常會統一而已,不這樣寫也能運作。
來看程式碼吧:
// ========== 有很多壞味道的購物車程式碼 ==========
// 【全域變數】所有資料都是全域的,容易被意外修改
var cartTotal = 0;
var itemPrice = [
{name: "遙控車", price: 250},
{name: "玩偶", price: 180},
{name: "拼圖", price: 120}
]; // 商品價格
var amounts = [2, 0, -10]; // 購買數量
// 【超長函式】一個函式做太多事情
// 【有副作用】直接修改全域變數 cartTotal
// 【魔術數字】3 這個數字沒有說明意義
// 【命名不清】t 這個變數不知道是什麼
function calc() {
// 【副作用】直接修改全域變數
cartTotal = 0;
console.log("開始計算購物車總金額...");
// 【魔術數字】3 被寫死了
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();
// 【全域變數的問題】任何地方都能修改
console.log("目前總金額:" + cartTotal + " 元");
// 糟糕!全域變數被意外修改了
cartTotal = 999999;
console.log("被改掉了!現在總金額:" + cartTotal + " 元");
// 再執行一次
calc();
// 【沒有回傳值】函式沒有回傳值,只能靠全域變數取得結果
// 【無法重複使用】如果要計算另一個購物車,這個函式無法重用
我們希望把程式碼變漂亮,但是功能不變,這個行為有一個聽起來很帥的專有名詞,叫「重構」(Refactoring)。程式界有一本名字就叫「重構」的書,專門在講這個知識。
在這裡我們示範一下不靠 AI ,手工改 Code 的傳統技藝 (以後或許可以去宜蘭傳藝中心表演?)。
重要提醒:重構的第一步,就是把現在的程式碼,在版本控制裡「儲存版本」。這樣出錯時可以隨時倒轉回去。之後每修改一步成功時,都是「儲存版本」的好時機。
提示:雖然我已經試著讓每一步的改動儘可能的小,但下面每一步建議手動跟著操作看看,並且思考為什麼要這樣改是怎麼運作的,也可以跟老師或是 AI 討論看看。
依賴全域變數的程式是非常脆弱的。只要把會影響到的變數放進計算的函式裡,然後把原先的全域變數設成回傳值,就能解決這個問題。
var itemPrice = [
{name: "遙控車", price: 250},
{name: "玩偶", price: 180},
{name: "拼圖", price: 120}
]; // 商品價格
var amounts = [2, 0, -10]; // 購買數量
// 接受外部傳進來的參數
function calc(amounts) {
var cartTotal = 0;
console.log("開始計算購物車總金額...");
// 【魔術數字】3 被寫死了
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("========================");
return cartTotal;
}
// 執行計算,把 amounts 傳進去
let total = calc(amounts);
console.log("目前總金額:" + total + " 元");
// 沒有全域變數 cartTotal 了
// 這樣就不會有副作用的問題了
// 也不會被下面這行程式碼影響
cartTotal = 999999;
// 再執行一次
total = calc(amounts);
接著由於打折這塊可以獨立運作,我們把這部份單獨抽出去做成函式。然後在用得到的地方呼叫新函式就好。
var itemPrice = [
{name: "遙控車", price: 250},
{name: "玩偶", price: 180},
{name: "拼圖", price: 120}
]; // 商品價格
var amounts = [2, 0, -10]; // 購買數量
// 【魔術數字】3 這個數字沒有說明意義
// 【命名不清】t 這個變數不知道是什麼
function calc(amounts) {
// 【副作用】直接修改全域變數
var cartTotal = 0;
console.log("開始計算購物車總金額...");
// 【魔術數字】3 被寫死了
for (var i = 0; i < 3; i++) {
// 【命名不清】t 是什麼意思?
var t = itemPrice[i].price * amounts[i];
// 【副作用】修改全域變數
cartTotal = cartTotal + t;
}
// 打折的計算
cartTotal = discount(cartTotal);
// 【副作用】印出結果
console.log("購物車總金額:" + cartTotal + " 元");
console.log("========================");
return cartTotal
}
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;
}
// 執行計算
let total = calc(amounts);
console.log("目前總金額:" + total + " 元");
// 沒有全域變數 cartTotal 了
// 這樣就不會有副作用的問題了
// 也不會被下面這行程式碼影響
cartTotal = 999999;
// 再執行一次
total = calc(amounts);
item_price
跟 amount
不好用最上面的 item_price
是個陣列,而 amounts
輸入的順序一定要是 1. 遙控車, 2. 玩偶, 3. 拼圖 的順序才能正確運作。我們把它改成比較對的資料型別。
let itemPrice = {
"遙控車": 250,
"玩偶": 180,
"拼圖": 120
}
// 用"購物車"這個比較對的名字
let cart = [
{name: "遙控車", amount: 2},
{name: "玩偶", amount: 0},
{name: "拼圖", amount: -10}
]
這樣我們迴圈的部份也要跟著修改:
for (var i = 0; i < 3; i++) {
// JavaScript 裡厲害的解構賦值手法
let {name, amount} = cart[i];
// 因為用了字典,所以可以直接用 name 當 key 來取得價格
cartTotal = cartTotal + (itemPrice[name] * amount);
}
這個跟據我們的操作,選用比較正確的資料型別,讓程式更好運作的知識,有個專有名詞叫「資料結構」,之後還會再說明,先聽過就好。
今天的程式碼看太多了,休息一下,明天再繼續改。
我們今天學到了軟體品質這個字,知道我們在乎的是易讀、規格、意圖、可測試、完備、簡潔、可擴充性效率這些條件。
也知道有經驗的工程師是可以嗅出軟體的壞味道的。厲害的工程師能用非常短的程式碼就做出別人要寫很長的程式,而且功能一模一樣,甚至在未來會更好改。也學到了重構就是功能不變的情況下,把程式碼變漂亮。我們還講了資料結構這個酷炫名詞,聽過就好,不用記。
總結一下今天的程式碼最後的樣子,記得要「儲存版本」喔!
// 商品價格字典, 以後可以很方便的新增或修改商品跟價格
let itemPrice = {
"遙控車": 250,
"玩偶": 180,
"拼圖": 120
}
// 改成用物件,可以用 name 當 key 來取得 itemPrice 的價格
let cart = [
{name: "遙控車", amount: 2},
{name: "玩偶", amount: 0},
{name: "拼圖", amount: -10}
]
function calc(cart) {
var cartTotal = 0;
console.log("開始計算購物車總金額...");
// 【魔術數字】3 被寫死了
for (var i = 0; i < 3; i++) {
// 厲害的解構賦值手法
let {name, amount} = cart[i];
// 因為用了字典,所以可以直接用 name 當 key 來取得價格
cartTotal = itemPrice[name] * amount;
}
// 打折的計算
cartTotal = discount(cartTotal);
// 【副作用】印出結果
console.log("購物車總金額:" + cartTotal + " 元");
console.log("========================");
return cartTotal
}
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;
}
// 執行計算
let total = calc(cart);
console.log("目前總金額:" + total + " 元");