---
# System prepended metadata

title: （穎旻試教版本）第一堂 - 從入門到進階：BMI 計算機 Clean Code 版本
tags: ["前端家教師資培訓班\_- 2024"]

---

# （穎旻試教版本）第一堂 - 從入門到進階：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，體重過重！健康指數為澄色！
```