Clean Code 讀後心得以及案例分享

2022-07-31 · 4 min read

Experience
Featured Post

最近看完 Clean Code 這本書,但其實嚴格來說也不算完整看完,因為本書是以 Java 當作基礎撰寫,且又有許多物件導向程式設計的概念,對目前的我來說並不是非常熟悉,所以有很多內容是直接跳過的或是似懂非懂地看過去;即使如此,還是從中吸收到許多觀念。

我認為這本書的精華是 Ch.12 羽化 以及 Ch.17 程式碼的氣味與啟發,如果時間不夠的人又想了解 Clean Code 這本書在說什麼,可以直接翻閱這兩章:

  • Ch.12 羽化

    • 簡單設計四守則,包含 執行完所有的測試沒有重複的部分表達程式設計師的本意 以及 最小化類別與方法的數量
  • Ch.17 程式碼的氣味與啟發

    • 引用了 Martin Fowler 在重構這本書裡許多不同的 Code Smells,並加上本書作者 Robert 自己發現的 smells。

實際案例

簡單介紹完 Clean Code 書籍內容後,分享一下我在工作上遇到的實際案例,以下內容有包含 JavaScript 以及 React 的概念,如果你有相關經驗我相信不會太陌生,我在其中也有加入註解,希望可以幫助讀者更快理解。

這是一個處理匯入資料的 handler,原先是寫在 class component 裡面,所以內容有許多 this

handleImportContent = () => { const { I18N, dataType } = this.props; const { mergekey } = this.state; const fieldsMap = {}; // 上傳檔內容比對匯入資料的 key to key map Object.keys(this.state.fieldsMap).forEach((ele) => { if (this.state.fieldsMap[ele].value) { fieldsMap[ele] = this.state.fieldsMap[ele].value; } }); // 將 state 裡的內容拿出來存在 fieldsMap 這個變數裡 if (dataType === "audience") { if (!mergekey) { document .getElementById("select_field") .scrollIntoView({ behavior: "smooth", block: "center" }); // 如果 dataType 是 audience 但缺了 mergekey 這個資料就將畫面滑到特定位置 this.setState({ isMergekeyMissing: true }); // 這個 state 為 true 時會在畫面上顯示提示字 return; } const isFieldsMissingMergeKey = Object.values(fieldsMap).includes( mergekey ); // fieldsMap 裡的 values 是否具有 mergekey 這個值 if (!isFieldsMissingMergeKey) { let mergekeyI18N; switch (mergekey) { case "member_sn": mergekeyI18N = I18N.memberSn; break; case "line_id": mergekeyI18N = I18N.line; break; case "email": case "phone": mergekeyI18N = I18N[mergekey]; break; default: mergekeyI18N = mergekey; } // 用來決定 alert 內容的判斷式 sendMessage("alert/add", { message: { type: "error", message: `${I18N.missingSpecifiedField} : ${mergekeyI18N}`, }, }); // 用來顯示 alert 的 function return; } } this.importContent( this.state.filename, fieldsMap, this.props.dataType, this.state.mergekey, ); // 以上都通過了就 import this.setState({ mergekey: null }); // 成功執行匯出後就初始化 state };

當初在閱讀這個 function 的時候覺得可讀性不佳,在看懂之後就決定開始重構它,重構方向有以下幾點:

  • 封裝條件判斷
  • 函式只做一件事情
  • 重新命名

另外再將原先的 class component 改成 function component,故所有相關的 props 或 state 都已經在上層處理成變數。

此外,原先的條件判斷以及 alert 都一併寫在 handleImportContent 裡,這邊直接將該些內容拿出來寫成另外的函數。

function scrollToMissingMergekeyNotice() { document .getElementById("select_field") .scrollIntoView({ behavior: "smooth", block: "center" }); setIsMergekeyMissing(true); } function sendFieldsNotMatchMergeKeyAlert() { // 對照原先 if (!isFieldsMissingMergeKey) 裡的內容 const missingFieldText = { member_sn: I18N.memberSn, line_id: I18N.line, email: I18N[mergekey], phone: I18N[mergekey], }[mergekey] || mergekey; // 將原先以 switch 進行條件判斷的方式改以 hash map 呈現 sendMessage("alert/add", { message: { type: "error", message: `${I18N.missingSpecifiedField} : ${missingFieldText}`, }, }); } function isTypeAudiencePass(newFieldsMap) { if (!mergekey) { scrollToMissingMergekeyNotice(); return false; } const isFieldsMatchMergeKey = Object.values(newFieldsMap).includes( mergekey ); if (!isFieldsMatchMergeKey) { sendFieldsNotMatchMergeKeyAlert(); return false; } return true; }

所以重構完之後本來的 handler 會變成這樣:

const handleImportContent = () => { const newFieldsMap = Object.assign( {}, ...Object.entries(fieldsMap).map(([key, { value }]) => { if (value) { return { [key]: value }; } return null; }) ); // 對照原先 fieldsMap 的內容, // 這邊重新命名並以 functional programming 的方式處理 key to key map if (dataType === "audience" && !isTypeAudiencePass(newFieldsMap)) { // 這邊也可將 dataType === "audience" 封裝成變數 isAudienceDataType // 而原本裡面有一大塊 if 判斷的情況也封裝成 isTypeAudiencePass 來決定是否在此 return return; } importContent(filename, newFieldsMap, dataType, mergekey); // 對照原本的 this.props.importContent setMergekey(null); // 初始化 state };

總結

我目前的狀態是入行一年半,在閱讀 Clean Code 的過程看到許多負面教材時,心裡頭就有 wow 這種情況我也遇過 的想法,在看完本書後我對於 Clean Code 的一個簡單領悟是 不斷優化與重構你的程式碼

換句話說,寫完之後請重新審視自己寫的程式,是否有哪些地方可以重構?其中的演算法是否能夠優化?命名是否清晰表達其意義?...

在這不斷優化的過程中,我們寫出來的程式碼將會不斷趨近於 Clean Code。

這幾年我學到的前端處理條件判斷的方法用 JavaScript 實作優先佇列(Priority Queue)