# 第一堂 - Clean Code:從命名規範到函式管理,打造易讀易懂的程式碼 ## 開課提醒 1. 錄影 2. 自我介紹 3. 分享重要資訊 3-1. [小組建立+回報](https://discord.com/channels/801807326054055996/1269862621788770315/1280461047223812157) ![截圖 2024-09-04 下午4.55.07](https://hackmd.io/_uploads/ryxTljB30.png) 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)