# (穎旻試教版本)第一堂 - 從入門到進階: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 版本** **BMI 計算機** 請依照影片內容*與 [excel BMI 規則](https://drive.google.com/file/d/1fcZS3cY96I7Hu_i84hSUaXhowv4CafaY/view?usp=sharing)、*[流程圖](https://whimsical.com/bmi-TvvWaNiFkczvT3Q3pk35KS),依照以下三階段來練習此題目。 ```html 第一階段:請寫 printBmi 函式,並印出對應狀態 printBmi(178, 20) >> 印出 console.log 文字為「您的體重過輕」 printBmi(178, 70) >> 印出 console.log 文字為「您的體重正常」 printBmi(178, 85)>> 印出 console.log 文字為「您的體重過重」 printBmi(178, 90)>> 印出 console.log 文字為「您的體重輕度肥胖」 printBmi(178, 110)>> 印出 console.log 文字為「您的體重中度肥胖」 printBmi(178, 130)>> 印出 console.log 文字為「您的體重重度肥胖」 printBmi("身高","體重")>> 印出 console.log 文字為「您的數值輸入錯誤,請重新輸入」 ``` ```html 第二階段:請程式碼裡加入此變數,並嘗試運用此變數裡的資訊。 printBmi(178, 20) >> 印出 console.log 文字為「您的體重過輕,健康指數為藍色」 printBmi(178, 70) >> 印出 console.log 文字為「您的體重正常,健康指數為紅色」 printBmi(178, 85)>> 印出 console.log 文字為「您的體重過重,健康指數為澄色」 printBmi(178, 90)>> 印出 console.log 文字為「您的體重輕度肥胖,健康指數為黃色」 printBmi(178, 110)>> 印出 console.log 文字為「您的體重中度肥胖,健康指數為黑色」 printBmi(178, 130)>> 印出 console.log 文字為「您的體重重度肥胖,健康指數為綠色」 printBmi("身高","體重")>> 印出 console.log 文字為「您的數值輸入錯誤,請重新輸入」 ``` ```html 第三階段:儲存每筆計算資料,多一個變數為 bmiHistoryData,並賦予空陣列來儲存計算物件資料,若數值輸入錯誤,則不儲存。 printBmi(178, 20) >> 印出 console.log 文字為「您的體重過輕,健康指數為藍色」 printBmi(178, 70) >> 印出 console.log 文字為「您的體重正常,健康指數為紅色」 printBmi(178, 85)>> 印出 console.log 文字為「您的體重過重,健康指數為澄色」 showHistoryData() >> 印出 console.log 文字為「您總共計算 3 次 BMI 紀錄,最後一次 BMI 指數為 26.83,體重過重!健康指數為澄色!」 ``` ## [3-1. 洧杰老師的範例程式碼:增加可讀性和可重構性](https://codepen.io/hexschool/pen/VwmGZBd?editors=0010) 1. **結合 Clean Code 規範,發展出更好的程式碼** **函式命名:對應 Clean Code 原則「函式名稱應該說明它做的內容」** `bmiStatesText`→ `printBmiStateText` 用動詞開頭 2. **判斷可以簡化:對應 Clean Code 原則「移除重複的程式碼」** `18.5 <= bmi && bmi < 24`→ `bmi < 24` 3. **提出重複的程式碼:對應 Clean Code 原則「移除重複的程式碼」** `addData` 和 `bmiStatesText` 幾乎每個判斷都重寫一次,可以拿出來放在判斷結束的下方執行 4. **提取變數:對應 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. 功能模組化 如果是自己依照題目寫出來的話,程式碼很可能 - 只會依照題目要求拆出 `printBmi` 和 `showHistoryData` 兩個函式 - `printBmi` 函式內容很長 ```jsx let bmiHistoryData = []; const bmiStatesData = { "overThin": { "state": "過輕", "color": "藍色" }, "normal": { "state": "正常", "color": "紅色" }, "overWeight": { "state": "過重", "color": "澄色" }, "mildFat": { "state": "輕度肥胖", "color": "黃色" }, "moderateFat": { "state": "中度肥胖", "color": "黑色" }, "severeFat": { "state": "重度肥胖", "color": "綠色" }, } function printBmi(height, weight) { const bmi = (weight / ((height / 100) * (height / 100))).toFixed(2); let bmiState = ''; if (bmi < 18.5) { bmiState = 'overThin'; } else if (bmi < 24) { bmiState = 'normal'; } else if (bmi < 27) { bmiState = 'overWeight'; } else if (bmi < 30) { bmiState = 'mildFat'; } else if (bmi < 35) { bmiState = 'moderateFat'; } else if (bmi >= 35) { bmiState = 'severeFat'; } else { console.log('您的數值輸入錯誤,請重新輸入'); } let obj = {}; obj.bmi = bmi; obj.state = bmiState; bmiHistoryData.push(obj); console.log(`您的體重${bmiStatesData[bmiState].state},健康指數為${bmiStatesData[bmiState].color}`) } function showHistoryData(){ const totalRecord = bmiHistoryData.length; const lastRecord = totalRecord - 1; const lastState = bmiHistoryData[lastRecord].state console.log(totalRecord, lastRecord, lastState); console.log(`您總共計算 ${totalRecord} 次 BMI 紀錄,最後一次 BMI 指數為 ${bmiHistoryData[lastRecord].bmi},體重${bmiStatesData[lastState].state}!健康指數為${bmiStatesData[lastState].color}!`); } // 以下需一行一行執行 // printBmi(178, 20); // 您的體重過輕,健康指數為藍色 // printBmi(178, 70); // 您的體重正常,健康指數為紅色 // printBmi(178, 85); // 您的體重過重,健康指數為澄色 // showHistoryData(); // 您總共計算 3 次 BMI 紀錄,最後一次 BMI 指數為 26.83,體重過重!健康指數為澄色! ``` 1. **實作功能拆分→ 對應 Clean Code 原則「一個函式只做一件事情」** - 計算 BMI 值 → `calculateBMI` - 印出「您的體重 xx,健康指數為 xx」文字 → `printBmi` - 條件判斷 BMI 結果是在哪個狀態 → `getBMIState` - 將每次結果記錄下來 → `addRecord` - 印出歷史紀錄 → `showHistoryData` ```jsx let bmiHistoryData = []; const bmiStatesData = { "overThin": { "state": "過輕", "color": "藍色" }, "normal": { "state": "正常", "color": "紅色" }, "overWeight": { "state": "過重", "color": "澄色" }, "mildFat": { "state": "輕度肥胖", "color": "黃色" }, "moderateFat": { "state": "中度肥胖", "color": "黑色" }, "severeFat": { "state": "重度肥胖", "color": "綠色" }, } function calculateBMI(height, weight) { const bmi = (weight / ((height / 100) * (height / 100))).toFixed(2); return bmi; } function getBMIState(bmi) { let bmiState = ''; if (bmi < 18.5) { bmiState = 'overThin'; } else if (bmi < 24) { bmiState = 'normal'; } else if (bmi < 27) { bmiState = 'overWeight'; } else if (bmi < 30) { bmiState = 'mildFat'; } else if (bmi < 35) { bmiState = 'moderateFat'; } else if (bmi >= 35) { bmiState = 'severeFat'; } return bmiState; } function addRecord(bmi, state) { let obj = {}; obj.bmi = bmi; obj.state = state; bmiHistoryData.push(obj); } 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(){ const totalRecord = bmiHistoryData.length; const lastRecord = totalRecord - 1; const lastState = bmiHistoryData[lastRecord].state console.log(totalRecord, lastRecord, lastState); return `您總共計算 ${totalRecord} 次 BMI 紀錄,最後一次 BMI 指數為 ${bmiHistoryData[lastRecord].bmi},體重${bmiStatesData[lastState].state}!健康指數為${bmiStatesData[lastState].color}!`; } // 以下需一行一行執行 // printBmi(178, 20); // 您的體重過輕,健康指數為藍色 // printBmi(178, 70); // 您的體重正常,健康指數為紅色 // printBmi(178, 85); // 您的體重過重,健康指數為澄色 // showHistoryData(); // 您總共計算 3 次 BMI 紀錄,最後一次 BMI 指數為 26.83,體重過重!健康指數為澄色! ```