iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0

你有沒有聽過軟體工程師討論時,會講到「這段程式碼有壞味道」?程式碼又不是食物,怎麼會有壞味道呢?我們在上一篇討論了軟體工程裡的「版本控制」,而工程師們討論時說的壞味道,就是軟體工程中,討論這段程式碼的品質好不好。有經驗的工程師可以在很短的時間內,看出程式碼品質的好壞,就像厲害的廚師,可以分辨出一道料理的好壞一樣。

程式碼品質在乎的點大概是下列這幾項:

  • 易讀:人類好不好懂
  • 規格:這段程式碼是不是很容易看懂要怎麼用?或是有沒有好的說明書?
  • 意圖:能不能從程式碼本身看出每一行想要處理什麼情況?
  • 完備:是否處理到各種可能發生的情況了?
  • 可測試:有沒有辦法重覆且自動來確認程式的正確性
  • 簡潔:程式的邏輯條件是乾淨清楚的,還是亂成一團容易出錯的?
  • 可維護性:有錯的話容易發現嗎?容易找到出錯的地方嗎?好不好修正?
  • 可擴充性:如果是會一直使用的程式的話,要加新功能會需要大幅度改寫嗎?
  • 效率:跑起來快不快

當然在生成式 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_priceamount 不好用

最上面的 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 + " 元");

地圖


上一篇
Ch 18. 用中文也可以做版本控制
下一篇
Ch 20. 最簡潔的列出所有可能的情況
系列文
Just enough code with AI: 給新手們的程式設計世界觀21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言