# 第一堂:從入門到進階:BMI 計算機 Clean Code 版本 ## 課程大綱 1. Clean Code 介紹 1. 1 個觀念- 5min 2. 變數 1. 3 個觀念 - 20min 3. 函式 1. 3 個觀念 - 20min 4. 中場休息 - 5min 5. BMI 計算機 Clean Code 版本 1. 2 份程式碼 - 30min ## Clean Code 介紹  [**3 Rs 程式架構**](https://github.com/ryanmcdermott/3rs-of-software-architecture) - 可讀性(Readability)- 讓別人好讀你的程式碼 - 可重用性(Reusability)- 減少重複的程式碼 - 可重構性(Refactorability) - 模組化 - 可維護性 - 可擴展性 ## 1. 變數 ### 1-1. 使用具有意義且可閱讀的名稱 **糟糕的程式碼範例:** 這段程式碼中使用了 **`yyyymmdstr`** 這樣的變數名稱,這樣的命名方式雖然包含了一些日期格式的資訊,但對於閱讀者來說不夠直觀,也難以理解其真正含義。 ```jsx const yyyymmdstr = moment().format('YYYY/MM/DD'); ``` **好的程式碼範例:** 這段程式碼改用了 **`currentDate`** 作為變數名稱,這樣的命名直接反映了這個變數儲存的是當前的日期,讓意圖更一目了然。 ```jsx const currentDate = moment().format('YYYY/MM/DD'); ``` ⭐️小練習:以此原則,請調整以下程式碼為更有意義且可閱讀的名稱 ```jsx 1) const data = ['red', 'white', 'black']; // 無法點擊的按鈕狀態 2) let status = 'disabled'; // 註冊時需填寫的資訊 3) let usr = { account: '', name: '', psd: '', } 4) let loading = false; // bmi 公式 5) const result = w / (h / 100) **2; ``` ### 1-2. 使用可搜尋的名稱 **糟糕的程式碼範例:** 在這個範例中,使用了一個不清楚的常數 **`1.07`**,代表了某種計算中的倍數或者增加的百分比,但這個數字本身並沒有清楚表達具體含義。 ```jsx function calculateTotal(price) { return price * 1.07; // 假設 1.07 代表含稅價格 } ``` **好的程式碼範例:** 在改進後的範例中,我們使用 `taxRate` 設定為 **`0.07`**,以表示稅率為 7%。然後在計算總金額時使用這個常數。這樣做可以讓後續閱讀程式碼的開發者更容易理解他的意圖和功能,並且在需要查找特定用途時更方便。 ```jsx const taxRate = 0.07; function calculateTotal(price) { return price * (1 + taxRate); // 使用常數使計算含稅價格更清楚 } ``` ⭐️小練習:以此原則,請調整以下程式碼為可搜尋的名稱 ```jsx // 0.5 秒後頁面重整 setTimeout(() => { window.location.reload(); }, 500); ``` ### 1-3. 使用可解釋的變數 **糟糕的程式碼範例:** 在這個例子中,雖然這段程式碼是正確的,但 **`x`** 和 **`y`** 並沒有提供足夠的上下文來解釋它們代表的具體含義,這使得理解真正意圖變得困難。 ```jsx const data = [{ x: 10, y: 20 }, { x: 20, y: 30 }]; let result = []; data.forEach(item => { result.push(item.x * item.y); }); ``` **好的程式碼範例:** 在改進後的例子中,我們通過更具描述性的變數名稱來明確每個屬性的含義 ```jsx const orders = [{ quantity: 10, unitPrice: 20 }, { quantity: 20, unitPrice: 30 }]; let totalCosts = []; orders.forEach(order => { totalCosts.push(order.quantity * order.unitPrice); }); ``` ⭐️小練習:以此原則,請調整以下程式碼為可解釋的名稱 ```jsx // 九九乘法表 const size = 9; for (let i = 1; i <= size; i++) { let row = ''; for (let j = 1; j <= size; j++) { row += `${i} * ${j} = ${i * j}\\t`; } console.log(row); } 英文單字提示:multiplier 表示乘數,multiplicand 表示被乘數,product 代表乘積 ``` ## 2. 函式 ### 2-1. 一個函式只做一件事情(單一性) **糟糕的程式碼範例:** 這個範例中`createUserProfile` 函式負責四件事: 1. 處理用戶名稱的空白 2. 檢查名稱長度 3. 解析和驗證年齡 4. 檢查電子郵件格式 這樣混合了太多責任的設計使得函式難以閱讀和維護,也增加了錯誤發生的機會。當一個函式做超過一件事情時,它會更難以被理解。 ```jsx // 建立用戶檔案,包含資料驗證和錯誤處理 function createUserProfile(userDetails) { const name = userDetails.name.trim(); // 正規化名字 if (name.length < 2) { console.log("錯誤:名稱至少需兩個字元長。"); return; } const age = parseInt(userDetails.age); // 轉換年齡 if (isNaN(age) || age < 18) { console.log("錯誤:您至少需要18歲。"); return; } if (!userDetails.email.includes('@')) { // 驗證電子郵件 console.log("錯誤:無效的電子郵件地址。"); return; } console.log("用戶檔案創建成功。"); // 成功訊息 } // 使用範例 createUserProfile({ name: "張三", age: "19", email: "zhangsan@example.com" }); ``` **好的程式碼範例:** 在這個重構的範例中,將功能拆分成更小的部分,每個函式專注於一個任務,提升了程式碼的清晰度和可維護性: ```jsx // 建立用戶檔案,包含資料驗證和錯誤處理 // 去除名稱中的前後空白 function trimName(name) { return name.trim(); } // 驗證名稱至少需要兩個字元 function isNameValid(name) { return name.length >= 2; } // 解析並驗證年齡,確認其為合法數字且至少為 18 歲 function parseAndValidateAge(age) { const parsedAge = parseInt(age); return !isNaN(parsedAge) && parsedAge >= 18; } // 確認電子郵件格式是否包含 '@' 符號 function isEmailValid(email) { return email.includes('@'); } // 處理創建用戶檔案的整個過程 function createUserProfile(name, age, email) { const normalizedName = trimName(name); if (!isNameValid(normalizedName)) { console.log("錯誤:名稱至少需兩個字元長。"); return; } if (!parseAndValidateAge(age)) { console.log("錯誤:您至少需要18歲。"); return; } if (!isEmailValid(email)) { console.log("錯誤:無效的電子郵件地址。"); return; } console.log("用戶檔案創建成功。"); } ``` ⭐️小練習:以此原則,請調整以下程式碼,保持函式的單一性 ```jsx function manageTodoList(todoList) { // 更新待辦事項清單 todoList.forEach((todo, index) => { if (!todo.completed) { todo.completed = true; } }); // 通知用戶 console.log('已完成的待辦事項清單:', todoList); } ``` ### 2-2. 函式名稱應該說明它做的內容 **糟糕的程式碼範例:** 在這個例子中,函式名稱 `updateInfo`,描述函式的功能不明確,update 會覺得是要更新某個資訊,但也無法得知要更新什麼資訊 ```jsx function updateInfo(info, data) { // 函式內容... } ``` **好的程式碼範例:** 在這個重構後的程式碼中,將函式名稱從`updateInfo`改為`mergeInfoWithNewData`,更清楚地表明了函式的功能,其實是要將現有資訊與新資料進行合併。這樣的命名方式使得程式碼更容易理解。 ```jsx function mergeInfoWithNewData(existingInfo, newData) { // 函式內容... } ``` 補充:建議函式命名以動詞開頭,有許多業界常用的字詞可以當作優先選擇 - get: 取得、取出 - set: 賦予、修改 - add: 增加 - remove: 排除 - is: 判定 - do: 執行邏輯 ### 2-3. 移除重複的程式碼 **糟糕的程式碼範例:** 兩個函式分別計算了電子產品和服裝的折扣價格,但計算邏輯大部分是一樣的,增加了程式碼的冗長度。如果未來需要增加更多類型的商品並計算其折扣價格,就需要再次複製貼上類似的函式,只會增加更多的重複性。 ```jsx function calculateElectronicsDiscount(price) { const discountPercentage = 0.1; // 電子產品的折扣率為10% return price * (1 - discountPercentage); } function calculateClothingDiscount(price) { const discountPercentage = 0.2; // 服裝的折扣率為20% return price * (1 - discountPercentage); } ``` **好的程式碼範例:** 將兩個函式中重複的部分抽取出來,並將可變的部分作為參數。 ```jsx function calculateDiscount(price, discountPercentage) { return price * (1 - discountPercentage); } const electronicsDiscountedPrice = calculateDiscount(price, 0.1); const clothingDiscountedPrice = calculateDiscount(price, 0.2); ``` ⭐️小練習:以此原則,請調整以下程式碼,提取重複內容管理 ```jsx function printInfo(person) { console.log(`Name: ${person.name}`); console.log(`Age: ${person.age}`); } function printStudentInfo(student) { console.log(`Name: ${student.name}`); console.log(`Age: ${student.age}`); console.log(`Major: ${student.major}`); } ``` ## **3. BMI 計算機 Clean Code 版本** ### [3-1. 洧杰老師 LV3 範例程式碼](https://codepen.io/hexschool/pen/VwmGZBd?editors=0010) **結合 Clean Code 規範,發展出更好的程式碼** **函式命名:對應 Clean Code 原則「函式名稱應該說明它做的內容」** `bmiStatesText`→ `printBmiResult` 並用動詞開頭 **判斷可以簡化:對應 Clean Code 原則「移除重複的程式碼」** `18.5 <= bmi && bmi < 24`→ `bmi < 24` **提出重複的程式碼:對應 Clean Code 原則「移除重複的程式碼」** `addData` 幾乎每個判斷都重寫一次,可以拿出來放在判斷結束的下方執行 **提取變數:對應 Clean Code 原則「使用具有意義且可閱讀的名稱」** `totalNum` → `totalRecord` 可以看出是記錄總量 `lastNum` → `lastRecord` 可以看出是最後一筆紀錄 ```jsx const totalRecord = bmiHistoryData.length; const lastRecord = totalRecord - 1; const lastState = bmiHistoryData[lastRecord].state; console.log(`您總共計算 ${totalRecord} 次 BMI 紀錄,最後一次 BMI 指數為 ${bmiHistoryData[lastRecord].bmi},體重${bmiStatesData[lastState].state}!健康指數為${bmiStatesData[lastState].color}!`); ``` ### [3-2. 常見學生範例程式碼](https://codepen.io/hexschool/pen/NWZgREV?editors=1011) 一開始自己寫完的 LV3,大概會長這樣: - 只會依照題目要求拆出 `printBmi` 和 `showHistoryData` 兩個函式 - `printBmi` 函式內容很長 ```jsx const bmiStatesData = { overThin: { state: "過輕", color: "藍色" }, normal: { state: "正常", color: "紅色" }, overWeight: { state: "過重", color: "澄色" }, mildFat: { state: "輕度肥胖", color: "黃色" }, moderateFat: { state: "中度肥胖", color: "黑色" }, severeFat: { state: "重度肥胖", color: "綠色" }, }; let records = []; function printBmi(height, weight) { const bmi = (weight / ((height / 100) * (height / 100))).toFixed(2); let state = ''; if (bmi < 18.5) { state = 'overThin'; } else if (bmi < 24) { state = 'normal'; } else if (bmi < 27) { state = 'overWeight'; } else if (bmi < 30) { state = 'mildFat'; } else if (bmi < 35) { state = 'moderateFat'; } else if (bmi >= 35) { state = 'severeFat'; } if (!state) { return '您的數值輸入錯誤,請重新輸入'; } records.push({ bmi: bmi, state: bmiStatesData[state].state, color: bmiStatesData[state].color }); return `您的體重${bmiStatesData[state].state},健康指數為${bmiStatesData[state].color}`; } function showHistoryData() { return `您總共計算 ${records.length} 次 BMI 紀錄,最後一次 BMI 指數為 ${records[records.length - 1].bmi},體重${records[records.length - 1].state}!健康指數為${records[records.length - 1].color}!`; } // 以下需一行一行執行 printBmi(178, 20); // 您的體重過輕,健康指數為藍色 printBmi(178, 70); // 您的體重正常,健康指數為紅色 printBmi(178, 85); // 您的體重過重,健康指數為澄色 showHistoryData(); // 您總共計算 3 次 BMI 紀錄,最後一次 BMI 指數為 26.83,體重過重!健康指數為澄色! ``` **為什麼這段程式碼還不夠好?** 多運用函式的優點,找出更合適的寫法: 1. ✅ (已做到)可重用性:因需要重複計算不同人的 BMI,因此會將 BMI 計算功能封裝在一個函式中重複利用,而不需要重複寫計算 BMI 邏輯,只要呼叫函式即可 2. 增加可讀性:函式因為模組化將功能獨立出來,讓整體結構更清晰,閱讀也會更容易理解和維護 3. 可重構性:讓程式碼方便維護,好擴展 **分析 BMI 功能,將程式碼做模組化並提高可讀性** ```jsx 第三階段:儲存每筆計算資料,多一個變數為 bmiHistoryData,並賦予空陣列來儲存計算物件資料,若數值輸入錯誤,則不儲存。 printBmi(178, 20) >> 印出 console.log 文字為「您的體重過輕,健康指數為藍色」 printBmi(178, 70) >> 印出 console.log 文字為「您的體重正常,健康指數為紅色」 printBmi(178, 85)>> 印出 console.log 文字為「您的體重過重,健康指數為澄色」 showHistoryData() >> 印出 console.log 文字為「您總共計算 3 次 BMI 紀錄,最後一次 BMI 指數為 26.83,體重過重!健康指數為澄色!」 ``` 如何規劃函式模組化?依照單一功能去發想: - 計算 BMI 值 → `calculateBMI` - 印出「您的體重 xx,健康指數為 xx」文字 → `printBmi` - 條件判斷 BMI 結果是在哪個狀態 → `getBMIState` - 將每次結果記錄下來 → `addRecord` - 印出歷史紀錄 → `showHistoryData` **實作功能拆分→ 對應 Clean Code 原則「一個函式只做一件事情」** ```jsx const bmiStatesData = { overThin: { state: "過輕", color: "藍色" }, normal: { state: "正常", color: "紅色" }, overWeight: { state: "過重", color: "澄色" }, mildFat: { state: "輕度肥胖", color: "黃色" }, moderateFat: { state: "中度肥胖", color: "黑色" }, severeFat: { state: "重度肥胖", color: "綠色" }, }; let records = []; function calculateBMI(height, weight) { const bmi = (weight / ((height / 100) * (height / 100))).toFixed(2); return bmi; } function getBMIState(bmi) { let state = ''; if (bmi < 18.5) { state = 'overThin'; } else if (bmi < 24) { state = 'normal'; } else if (bmi < 27) { state = 'overWeight'; } else if (bmi < 30) { state = 'mildFat'; } else if (bmi < 35) { state = 'moderateFat'; } else if (bmi >= 35) { state = 'severeFat'; } return state; } function addRecord(bmi, state) { const bmiColor = bmiStatesData[state].color; const bmiState = bmiStatesData[state].state; records.push({ bmi: bmi, state: bmiState, color: bmiColor }); } function printBmi(height, weight) { const bmi = calculateBMI(height, weight); const state = getBMIState(bmi); if (!state) { return '您的數值輸入錯誤,請重新輸入'; } addRecord(bmi, state); return `您的體重${bmiStatesData[state].state},健康指數為${bmiStatesData[state].color}`; } function showHistoryData() { return `您總共計算 ${records.length} 次 BMI 紀錄,最後一次 BMI 指數為 ${records[records.length -1].bmi},體重${records[records.length -1].state}!健康指數為${records[records.length -1].color}!`; } ```