# 第一堂 - Clean Code:從命名規範到函式管理,打造易讀易懂的程式碼
## 開課提醒
1. 錄影
2. 自我介紹
3. 分享重要資訊
3-1. [小組建立+回報](https://discord.com/channels/801807326054055996/1269862621788770315/1280461047223812157)

3-2. [Teachable 影音課程介紹](https://courses.hexschool.com/courses/20201113112/lectures/55748824)
3-3. [課程時間軸](https://www.hexschool.com/2024/07/22/2024-07-22-js-clean-code-tutor-training/)
4. 課程介紹
## 今日上課知識點
1. Clean Code - 變數命名
2. Clean Code - if
3. Clean Code - 函式
---
## Clean Code
大部分初學者 or 沒有經過訓練的工程師程式碼品質:
- 當寫出來的程式一團糟
<img src="https://hackmd.io/_uploads/HychPZf7A.png" width="250">
- 千萬不能動的程式碼
<img src="https://hackmd.io/_uploads/rkxxObfXA.png" width="350">
- 回來維護或重構程式碼時,對程式碼完全沒印象
<img src="https://hackmd.io/_uploads/SyxGdWMm0.png" width="350">
Clean Code 可以讓程式碼更為整潔易讀、增加可維護性
主要原則如下:
- 保持程式碼簡單和直觀:避免過於複雜的設計導致閱讀性很差
- 表達單一意圖:程式碼專注一件事,你看到的程式執行過程與結果會和函式名稱差不多
- 抽象化:程式碼不重複,透過抽象概念來重用
## 變數命名、if、函式 Clean Code 原則
### 變數命名
### 一、使用具有意義且可閱讀的名稱
**糟糕的程式碼範例:**
這段程式碼中使用了 **`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;
```
### 二、相同類型的變數使用相同的名稱
**糟糕的程式碼範例:**
這裡有三個函式,它們都是用來發送網路請求並獲取資料用,但是它們的命名方式各不相同,這會導致可讀性變差。
```jsx
function queryDatabase(query) {
// 向資料庫發送查詢的邏輯
}
function fetchPage(url) {
// 獲取網頁內容的邏輯
}
function retrieveAPI(endpoint) {
// 從API端點檢索資料的邏輯
}
```
**好的程式碼範例:**
在這個改進後的範例中,我們使用 **`fetchData`** 作為函式名稱,以統一表示所有的網路請求操作。函式的參數可以用來指定具體的查詢、URL 或 API,使函式更加通用且一致。
```jsx
function fetchData(source, identifier) {
// 根據不同的來源和 id 發送請求和獲取資料的邏輯
// 'source' 可以是 'database', 'webPage', 'api' 等
// 'identifier' 可以是查詢字串、URL 或 API 等
}
```
### 三、使用可搜尋的名稱
**糟糕的程式碼範例:**
在這個範例中,使用了一個不清楚的常數 **`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);
```
### 四、使用可解釋的變數
**糟糕的程式碼範例:**
在這個例子中,雖然這段程式碼是正確的,但 **`x`** 和 **`y`** 並沒有提供足夠的上下文來解釋它們代表的具體含義,這使得理解真正意圖變得困難。
```jsx
const data = [{ x: 10, y: 20 }, { x: 20, y: 30 }];
const result = data.map(item => item.x + item.y);
```
**好的程式碼範例:**
在改進後的例子中,我們通過更具描述性的變數名稱來明確每個屬性的含義
```jsx
const orders = [{ quantity: 10, unitPrice: 20 }, { quantity: 20, unitPrice: 30 }];
const totalCosts = orders.map(order => 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 代表乘積
```
### 五、避免心理作用
**糟糕的程式碼範例:**
在這個例子中,使用了 **`e`** 作為事件處理函式中的參數名稱。雖然在某些情境下這是一種常見的簡寫,但它可能會導致意圖不夠明確,特別是在較長或更複雜的函式中。
```jsx
document.getElementById('myButton').addEventListener('click', e => {
console.log(e.target);
// 做一些事情...
// ...
// 等等,`e` 是什麼?
});
```
**好的程式碼範例:**
在改進後的例子中,我們將 **`e`** 改為 **`event`**,明確表達是在處理一個事件對象。這樣一來,即使在程式碼較長或較複雜的情況下,其含義也依然清晰易懂。
```jsx
document.getElementById('myButton').addEventListener('click', event => {
console.log(event.target);
// 做一些事情...
// 現在知道 `event` 指的是什麼了
});
```
⭐️小練習:以此原則,請調整以下程式碼避免心理作用
```jsx
const arr = [
{ id: 'item1', name: 'Smartphone', price: 8500 },
{ id: 'item2', name: 'Refrigerator', price: 15000 },
{ id: 'item3', name: 'Tablet', price: 5000 }
];
const displayArr = arr.map(item => item.name).join('、');
```
### if
### 一、區分不同邏輯的 if/else
**糟糕的程式碼範例:**
這個範例展示了一個包含兩個邏輯的多層巢狀 **`if-else`** 語句,這使得程式碼的閱讀變得困難。
```jsx
function processUserInput(input) {
if (input.type === 'text') {
if (input.value === 'hello') {
return 'Hi there!';
} else if (input.value === 'bye') {
return 'Goodbye!';
} else {
return 'Unrecognized greeting';
}
} else if (input.type === 'number') {
if (input.value < 10) {
return 'Number is too small';
} else {
return 'Number is big enough';
}
} else {
return 'Unsupported input type';
}
}
```
**好的程式碼範例:**
在改進後的例子中,將文字輸入和處理數字輸入的邏輯分開管理來簡化流程,減少了巢狀結構。此外,如果未來還想增加判斷內容也不會讓邏輯變得複雜,因為我們只需要擴展對應的內容,而不需要修改現有的條件判斷結構。
```jsx
// 處理文字輸入
const textProcessors = {
'hello': () => 'Hi there!',
'bye': () => 'Goodbye!'
};
// 處理數字輸入
const numberProcessor = (value) => value < 10 ? 'Number is too small' : 'Number is big enough';
function processUserInput(input) {
if (input.type === 'text') {
return textProcessors[input.value] ? textProcessors[input.value]() : 'Unrecognized greeting';
} else if (input.type === 'number') {
return numberProcessor(input.value);
} else {
return 'Unsupported input type';
}
}
```
💡補充:early return 可以讓邏輯更直接,減少嵌套結構,如果邏輯有問題也會更好發現和調整
```jsx
function processUserInput(input) {
if (input.type === 'text') {
// 檢查文字輸入是否在 textProcessors 中
if (textProcessors[input.value]) {
return textProcessors[input.value]();
}
return 'Unrecognized greeting';
}
if (input.type === 'number') {
// 直接處理數字輸入
return numberProcessor(input.value);
}
// 處理不支援的輸入類型
return 'Unsupported input type';
}
```
### 二、太多的 if/else
**糟糕的程式碼範例:**
這個例子中使用了多個 **`if-else`** 來處理不同的狀態,雖然程式碼功能正常,但可讀性和可維護性不好。
```jsx
function getStatusMessage(status) {
if (status === 'success') {
return 'Operation was successful.';
} else if (status === 'error') {
return 'An error occurred.';
} else if (status === 'loading') {
return 'Loading...';
} else {
return 'Unknown status.';
}
}
```
**好的程式碼範例:**
透過將狀態和對應的訊息放到一個物件中,消除 **`if-else`** 結構,也使得新增或修改訊息更為方便,因為你只需要更新`statusMessages`,而不需要改函式的邏輯
```jsx
const statusMessages = {
'success': 'Operation was successful.',
'error': 'An error occurred.',
'loading': 'Loading...',
'default': 'Unknown status.'
};
function getStatusMessage(status) {
return statusMessages[status] || statusMessages['default'];
}
```
⭐️小練習:以此原則,請判斷以下程式碼是否可以省略 if/else,可以的話該如何調整
```jsx
function processPayment(paymentMethod) {
if (paymentMethod === 'credit_card') {
return processCreditCardPayment();
} else if (paymentMethod === 'paypal') {
return processPaypalPayment();
} else if (paymentMethod === 'bank_transfer') {
return processBankTransferPayment();
} else if (paymentMethod === 'cash') {
return processCashPayment();
} else {
return 'Invalid payment method';
}
}
```
### 三、你或許不需要 switch
**糟糕的程式碼範例(使用 `switch`):**
假設我們要根據用戶的角色類型來獲取對應的權限等級,當角色類型很多時,使用 **`switch`** 可能會導致代碼冗長且難以維護。
```jsx
function getPermissionLevel(role) {
switch (role) {
case 'admin':
return 10;
case 'manager':
return 7;
case 'editor':
return 5;
case 'user':
return 1;
default:
return 0;
}
}
```
**好的程式碼範例:**
將角色和權限等級放到一個物件中,當需要獲取某個角色的權限等級時,只需查找這個物件,如果角色不存在,則默認返回 **`0`。**也方便對角色權限的管理和擴展,比如新增或修改角色權限時,只需更新 **`rolePermissions`** 物件即可。
```jsx
const rolePermissions = {
'admin': 10,
'manager': 7,
'editor': 5,
'user': 1
};
function getPermissionLevel(role) {
return rolePermissions[role] || 0;
}
```
### 四、用三元運算子
**糟糕的程式碼範例:**
這個例子中使用了`if-else`結構來根據條件返回不同的值,這是可行的,但當條件相對簡單時,可以用更簡潔的方式來表達。
```jsx
function getUserStatus(isActive) {
if (isActive) {
return 'User is active.';
} else {
return 'User is inactive.';
}
}
```
**好的程式碼範例:**
使用三元運算符根據`isActive`的值來決定返回哪個字串,使內容更加簡潔明了。
不過值得注意的是,在處理更複雜的條件或執行多個操作時,過度使用三元運算符可能會降低代碼的可讀性。
```jsx
function getUserStatus(isActive) {
return isActive ? 'User is active.' : 'User is inactive.';
}
```
⭐️小練習:以此原則,請調整以下程式碼成三元運算子的寫法
```jsx
if (isChecked) {
window.location.href = `./training/board`;
} else {
window.location.href = `./training/welcome`;
}
```
### 函式
### 一、參數少於 2 個較佳
**糟糕的程式碼範例:**
在這個程式碼中,函式接受了五個單獨的參數。當參數變多時,很容易導致函式調用時的混淆,並且容易出錯。
```jsx
function showAlert(title, content, color, icon, closable) {
// 函式內容...
}
```
**好的程式碼範例:**
如果你有超過兩個以上的參數,代表你的函式做太多事情。如果無法避免時,可以有效地使用物件替代大量的參數。
這樣做的好處是在函式調用時可以更清楚指定參數。這也使得函式在增加新參數時更具彈性,因為它不會影響現有函式的程式碼。
```jsx
function showAlert({ title, content, color, icon, closable }) {
// 函式內容...
}
// 使用示例:
showAlert({
title: '警告',
content: '這是一個警告訊息!',
color: 'red',
icon: 'warning',
closable: true
});
```
補充:
此寫法可結合預設參數使用,是為了防止未定義或空值傳入引起的錯誤
```jsx
function showAlert({
title = 'Title',
content = '',
color = 'warning',
icon = 'info',
closable = true
} = {}) { // 如未傳遞任何參數使用預設空物件,使用 `= {}` 可避免 TypeError: Cannot destructure property `...` of 'undefined' or 'null'.
return {
title,
content,
color,
icon ,
closable
}
...
}
// 即使呼叫函式時缺少某些參數也不會導致錯誤
const alert = showAlert({
title: '警告',
content: '這是一個警告訊息!',
});
```
⭐️小練習:以此原則,請調整以下程式碼的參數用法
```jsx
function addTodo(title, content, status, category, priority) {
// 函式內容...
}
```
### 二、 一個函式只做一件事情(單一性)
**糟糕的程式碼範例:**
這個範例中,函式用來處理多種訂單狀態。當一個函式做超過一件事情時,它會更難以被理解。
```jsx
function processOrders(orders) {
orders.forEach(order => {
if (order.status === 'pending') {
processPendingOrder(order);
} else if (order.status === 'shipped') {
shipOrder(order);
} else if (order.status === 'delivered') {
sendDeliveryConfirmation(order);
}
});
}
```
**好的程式碼範例:**
將功能拆分為多個函式,每個函式只負責處理一種訂單狀態,專注於單一任務。
```jsx
function processPendingOrders(orders) {
orders.filter(isPendingOrder).forEach(processPendingOrder);
}
function shipOrders(orders) {
orders.filter(isShippedOrder).forEach(shipOrder);
}
function sendDeliveryConfirmations(orders) {
orders.filter(isDeliveredOrder).forEach(sendDeliveryConfirmation);
}
function isPendingOrder(order) {
return order.status === 'pending';
}
function isShippedOrder(order) {
return order.status === 'shipped';
}
function isDeliveredOrder(order) {
return order.status === 'delivered';
}
```
⭐️小練習:以此原則,請調整以下程式碼,保持函式的單一性
```jsx
function manageTodoList(todoList) {
// 更新待辦事項清單
todoList.forEach((todo, index) => {
if (!todo.completed) {
todo.completed = true;
}
});
// 通知用戶
console.log('已完成的待辦事項清單:', todoList);
}
```
### 三、 函式名稱應該說明它做的內容
**糟糕的程式碼範例:**
在這個例子中,函式名稱 `updateInfo`,描述函式的功能不明確,update 會覺得是要更新某個資訊,但也無法得知要更新什麼資訊
```jsx
function updateInfo(info, data) {
// 函式內容...
}
```
**好的程式碼範例:**
在這個重構後的程式碼中,將函式名稱從`updateInfo`改為`mergeInfoWithNewData`,更清楚地表明了函式的功能,其實是要將現有資訊與新資料進行合併。這樣的命名方式使得程式碼更容易理解。
```jsx
function mergeInfoWithNewData(existingInfo, newData) {
// 函式內容...
}
```
補充:建議函式命名以動詞開頭,有許多業界常用的字詞可以當作優先選擇
- get: 取得、取出
- set/update: 賦予、修改
- add: 增加
- remove: 排除
- is: 判定
- do: 執行邏輯
### 四、 移除重複的程式碼
**糟糕的程式碼範例:**
兩個函式分別計算了電子產品和服裝的折扣價格,但計算邏輯大部分是一樣的,增加了程式碼的冗長度。如果未來需要增加更多類型的商品並計算其折扣價格,就需要再次複製貼上類似的函式,只會增加更多的重複性。
```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}`);
}
```
## 作業講解
1. (必做)主線作業:購物車流程
2. 加碼:[Clean Code 修改練習](https://codepen.io/hexschool/pen/yLWYJaJ?editors=1010)
1. 將提示中的 Clean Code 原則,註解在使用的該行程式碼後面
2. 至少需要修改 3 個以上
提交格式:
1. 在 DC 討論串回報 Codepen 連結
3. 報名方案二的同學,可開始在同組尋找一起做「錄製試教影片任務」的夥伴,之後需要錄製三個試教影片,可先觀看洧杰的試教影片
4. 小組任務
想分組但沒有組別的人,請在今天填寫[分組表單](https://docs.google.com/forms/d/e/1FAIpQLScWaCtIh9NMd_lEpJkWxi5H5HP7X7kJd_1rhhNeg8uBiuhoiw/viewform)