讀書筆記《重構 改善既有程式碼的設計》(第2版本)

  • 2021 年 1 月 14 日
  • 筆記

前沿,印象

  重構是什麼,用來做什麼,怎麼做,what、why、how,而這一句話:便於理解,便於修改,是重構這個方法最直白的解釋了。當然,書裡面也包括日常遇到的每一個平平無奇的重構招式、方法背後,其實都有一些理論或者思想在支撐著,比如單一職責原則,以及其他幾個原則(開閉原則、里氏替換原則、介面隔離原則、依賴倒置等),這些都是前輩們總結出來的,遵循原則,讓寫出來的程式碼能夠高內聚、低耦合,軟體項目也能夠更加健壯,維持更好更長的生命周期。

  什麼樣的程式碼才是好的程式碼,其實這又回到程式語言本身了。程式語言的發展,從古早的機器語言(01),到後來的彙編語言(命令式),到高級語言,其實就是一個從複雜到簡單的過程,程式語言是讓人(程式設計師)來使用的,只有更多的人易於理解,才能更好的使用。如果論性能的話,自然是機器語言和彙編語言性能最好了,但是它們太難記憶了,也沒人能看得懂,因此才出現了高級語言。最好的理解的,自然是人類語言,但是又過於靈活,過於沒有規則,電腦又識別不了了,因此儘可能的接近人類語言,更易於理解的,就是更好的程式碼。用更高層次的抽象,來取代基本的控制結構,這樣可以讓開發者聚焦精力於業務場景而無需費心複雜底層運作。提升程式碼信噪比(簡潔性),盡量減少不確定因素來使程式碼極度簡潔。從這一點來說,偽程式碼反而是最好的程式碼,因為它不注重細節,注重業務,便於人們理解,「我不管你怎麼實現,我就要這種效果」,幾乎沒有bug。

  好的程式碼和壞的程式碼,也許更多的區別是通過一些細節來體現的,比如方法或變數的命名(這本書裡面把方法叫做函數了),比如一些很細微的習慣等等,這些小的綜合起來卻產生一些質的變化。類比一下就如同建設一棟大廈,品質好的建築,從一磚一瓦都可以看出來。

  這本書語言很平實,沒有糾結於講道理,講理論,或者是敘述程式碼歷史,而是通過一些很家常的例子,結合手法,潤物細無聲的把重構的精髓給講了出來,重點章節值得多次去品讀,結合一些設計模式,會有更深入的理解。

  當然也有一些缺點,比如例子用js寫的,某些特性是Js適用的,不好理解,有些翻譯腔有點重,不夠local,讀起來略吃力,翻譯不夠信達雅。

第1章 重構,第一個實例

  通過一個例子,讓人對重構有了個感性的認識,例子中用到了本書大部分的一些重構手法,比如提煉函數,內聯變數,搬移函數,用多態代替條件表達式等,看完之後,對重構有了個基本的了解了,哇,原來這就是重構,我也可以行。裡面用到的手法,可能自己平時就在用,某個方法或者經常用,或者不經常用,或者呢,偶爾也會用到這個手法。比如提煉函數,當被複制粘貼一段同樣的重複的程式碼折磨的不行的時候(通常會需要多處修改那種),也會給這段程式碼封裝一下,放在類的底部,用一個private修飾的方法,這樣這個類其他地方也可以用啦。再進一步呢,跨類跨模組也用到了,怎麼辦,那就放在logic里,成為一個public的方法咯,這樣其他模組也能用到啦。自我總結一下,是被動的去使用重構,或者說是不規律的去使用,沒有一個通用的或者是主動的去推動去使用。

  還有一個印象比較深的地方,就是小步快跑式的去重構,去修改,去迭代。第一章的這個例子,有時候看感覺好繁瑣啊,每次都動那麼一行程式碼,就像人說話很啰嗦的樣子,然而,這恰恰是這本書或者說是作者的良苦用心所在,這也是重構的精髓了,一方面重構一定要像搭積木從地基做起一樣,沒有副作用的去重構,小步快跑式的去做,要經常的運用重構,命名不合理,程式碼讓人理解費力,那麼就是重構的時機了。另一方面,每次都挪動一行程式碼的方法,其實最重要的還是在於理順思路,理解程式碼的邏輯,這也是重構的一個本質了。

  記得剛入職時的一個培訓,第一步就是了解需求,然後分解任務,設定驗收標準等,其中一句話記得挺久,編程人員千萬不要分配一個任務,立馬就去寫程式碼,而是要去分解,去理順思路,然後建工作項,分解任務,然後才是快樂的code。

第2章 重構的原則

  第一章已經對重構有了個基本的認知了,什麼是重構,就是那一點一點的移動啊、修改啊,平平無奇又看似無效率,看似簡單,其實背後有很多的理論參與進來。重構並不是有程式碼潔癖才去做,而是有真切的好處。就像一個人,平時一定要保持一些好的習慣(比如閱讀、健身、規劃、跑步等),這些好的習慣,會不知不覺影響你,讓你成功(聽起來很毒雞湯呢)。那麼在編程領域呢,一定要保持一些好的習慣、原則、思維、乃至思想,這些會影響你,讓你成為一個好的coder,重構或者說重構的這些手法,就是區分好的碼農和不好的碼農的所在了。

  何謂重構,兩頂帽子,重構既是名詞又是動詞,比如說把某個模組重構一下,用到了哪哪些重構手法。往大的講,重構可以作為編程人員的一個思維了,結合第三章的程式碼的壞味道,當你去做一個方法,一個模組的時候,就要帶著重構的思維去code,預見有哪些地方可能要重構等等,規避哪些壞味道。

  為何重構,沒有銀彈,想起《人月神話》,重構本身不是一蹴而就,也不是單個手法、技術,而是一系列組合,並且重構也可能在某個時間覺得這樣的重構好,在另外一個時間,可能覺得另一種更好,比如函數提煉,內聯變數等,可能相互轉換。重構的好處顯而易見,你好我也好,假如是一個人的項目,你自己可以更好的理解(常常我們自己寫的程式碼,幾個月或者幾天後,就會迷惑,需要重新釐清思路),假如是團隊項目,別人更容易理解了。

  何時重構,老話講的好,有再一再二沒有再三,我覺得這句話對於我來講過於真實了,每次自己做的重構,都是覺得不能再重複複製程式碼、多處修改程式碼,長痛不如短痛,然後提煉、封裝。預備性重構,大約講的是將重構變成一種習慣,每次寫程式碼都要帶著這個念頭、思維。

  怎樣對經理說,這也是一個矛盾點,大約是和唯效率論衝突,畢竟重構是在舊的程式碼上做一些加減法,短期內基本沒有產出,還不如修幾個Bug來的實在,沒有價值或者價值常常被忽略。其實重構常常會消滅一些潛在的bug,當然,最重要的好處,就在於軟體項目是一個工程,重構就是一個品質監督員,能夠使得品質更為良好,更為健壯。

第3章 程式碼的壞味道

  一個項目會腐爛變質,還是因為程式碼有壞味道,一點一滴積累起來,導致項目不堪負重,不得不推倒重來。程式碼的壞味道,大概就是程式設計師自我調侃的「祖傳程式碼」類似,祖傳程式碼不能動,輕則傷筋動骨,重則一命嗚呼。只能自己看懂,別人看不懂也不敢改。關於注釋,本書講的是,好的程式碼不應該有注釋,因為粒度夠細,夠抽象,程式碼自己就有解釋性,通過命名、上下文即可看出意圖、組什麼等,之前看另一本書,說的是好的項目應該有注釋或者文檔,恨不得每一行程式碼都要有注釋,又是走向另一個極端了,見仁見智。

  程式碼的壞味道這一章,從常見的那些不合理命名,到不合理的方法、邏輯等,只要讓人感覺到難以理解,其實都是程式碼不合理導致的。好的程式碼,應該是接近人類語言或者說人的思維,讓人能夠很順暢的讀懂、去理解程式碼是去做什麼的,怎麼做的。可讀性在某種意義上,是好程式碼和壞程式碼的區別。因為可讀性有時候是衡量程式碼有多好的很重要指標之一,可讀性最終決定了程式碼的可維護性,即使程式碼的唯一讀者是我們自己本人。我們常常試著去讀好久之前自己寫的程式碼,都有可能被繞暈,或者耗費大量的時間去復現去理清思路,當時基於什麼考慮等等,特例用來幹什麼,更不用說讓其他人來讀、來修改我們的程式碼了。出現問題時,當然是更簡介、更易讀的程式碼,更好去排查問題。

  看這一章時,確實能看的很慚愧,因為很多不合理的程式碼,自己經常在開發中出現,比如過長的函數、過大的類、過於複雜的循環、if…else等嵌套等,曾經做過一個方法,因為要判斷各類狀態,編寫完大約有五百行的程式碼,每當出現bug調試時,總是很崩潰,無從下手。程式碼的壞味道,根本原因還是在於開發這個功能時沒有想好,沒有規劃好業務邏輯,然後就急匆匆的下手編碼,導致枝杈橫生,邏輯複雜,不得不採用很多if…else判斷、嵌套、循環等來彌補。因此寫程式碼前一定要先思考,先分析邏輯,整理業務,必要時畫圖、流程圖等,總結之後,覺得邏輯沒有問題,才是下一步的寫程式碼。

第6章 第一組重構

  從本章開始,就是最常用的、也是比較精髓的一些重構手法了,剛開始看時是覺得很平平無奇的,很多就是那種挪一行程式碼,再挪一行程式碼,有些手法還是有對應的反過來的手法,比如內聯函數——提煉函數,走一步退一步等於沒走那種。難得之處就在於作者能夠手把手的一步一步的引導,不厭其煩的示範。這一章值得多次重複的去閱讀去理解,做筆記做小抄,並且要記下來養成習慣,平時主動去做。很多重構手法看起來很簡單,但是背後的思想或者說更深的理論,卻值得思考。所有的重構手法,都是服務於「易於理解,易於修改」這一點。重構是一步步向著粒度更小的程式碼前進,符合了單一職責原則。

  提煉函數,重構的第一步,避免過長的函數的剋星,首先分析業務邏輯,然後分拆不同的業務模組,分清做什麼,平時寫程式碼要養成寫非常小的函數的好習慣,通常只有幾行,超過六行就散發臭味。

  反向重構,內聯函數,如何掌握與提煉函數之間的度,不能為了重構而重構,過度重構,不然就變成一堆調用鏈了。

  提煉變數,將某些值提煉為有意義的變數,這樣一個好處是能夠調試(自己經常用到,用過之後再刪掉這個變數),另一個好處就是區分狀態值,明白這個邏輯是幹什麼用的,尤其是在if等的條件表達式中很有用,一堆的條件與或,通常讓人看得頭大,提煉變數就等於提煉邏輯出來了。

  反向重構,內聯變數,這裡要區分有用的變數和多餘的變數之間的關係,假如本身狀態值不多,或者邏輯不複雜,很容易就能看懂,沒有經過複雜的計算、與或合併等,那麼就不需要額外的變數了,額外的變數也是導致過長函數的一個原因吧。

  改變函數聲明,其實就是函數改名,類似的有變數改名,一個有意義的、邏輯清晰的命名,非常重要。相信每個人看到 var aaa = something肯定是崩潰的,混淆式命名大概屬於祖傳程式碼的一部分吧。一個好的命名是基礎,怎麼做、幹什麼的,能夠體現出來業務邏輯來,讓人明白無誤,無歧義。要用心命名,可能要經過多次修改。

  函數組合成類、函數組合成變化,講的都是將相似的、同類的業務邏輯合併,其實就是面向對象的封裝。

  拆分階段,和函數組合成類相反,本質還是理順業務邏輯,將不同的邏輯拆分出來,獨立成一個函數。

第7章 封裝

  封裝類、封裝函數、封裝記錄(數據)、封裝欄位,上一章是小打小鬧,一行一行搬移語句的話,這一章開始進行大的重構了,封裝、繼承、多態,是面向對象語言的三個基本特徵,本書裡面一些大的重構,也基本貫穿的就是這三個特徵了。其實封裝哪些類、封裝哪些函數,最重要的,就是拆分邏輯,將相似邏輯、相似功能的程式碼塊放在一起,進而隱藏細節的實現邏輯(比如一個循環、一個判斷等),將實現提升到高階,使用者無需關心內部實現,只需要使用即可。

  這一章完全就考驗的就是規劃能力了,哪些需要封裝,該如何對外暴露等等,多用多重構,熟練了就好。多去重構個幾次,就明白怎麼拆分業務邏輯,怎麼去拆分大的類、方法,然後封裝成小的類、精準的類。

看完這麼多封裝的手法,比如封裝記錄,封裝類、封裝函數,總結就是思想很重要,手法不重要,你是如何提煉功能,比你敲更快的程式碼更重要。

第8章 搬移特性

  這一章開始挪語句挪方法挪程式碼的乾坤大挪移了,如何挪,一句一句的挪,有什麼用,將功能相關的程式碼放在一起,整理在一起,在挪的過程,就是邏輯的整理過程,相同引用上下文的程式碼放在一起,下一步就是提煉邏輯、封裝函數、封裝類、封裝記錄。

  以管道取代循環,管道大概是js的一個特性,在c#中,大概就是lambda或者linq,這麼做有什麼好處,大概就是函數式編程的好處了吧,以前寫過關於List的ForEach方法的使用,集合List的ForEach用法:用函數式編程替代面向過程,好處就是更簡潔,更高階,關於函數式編程,以前也總結過,產品開發中的函數式編程思想,有現成的、更好的當然就用現成的了。

第9章 重組數據結構

  重組數據介面這一章基本就是圍繞數據結構比如欄位、記錄、變數等做重構,值對象改為引用對象、引用對象改為值對象,感覺是js里才有的,因為有作用域、閉包之類的。其他的變數重命名等,前幾章也講過,感覺這一章沒那麼重要。

第10章 簡化條件邏輯

  這一章和第6章、第7章、第8章,感覺是需要時不時的重看的章節,溫故而知新,每次看應該都有收穫,所有的重構手法都需要牢記在心,熟練運用。這一章對我而言也很震撼,在開發中犯的一些錯誤,也能一一找到解決方法,這才是最實用最能解決問題的鑰匙。比如if…else導致的過長的函數、過大的類、switch等,都獲益匪淺。衛語句與單一出口入口之爭,解決了嵌套過多的問題。總之這一章對實際工作中很有指導意義,解決了一些燃眉之急,相比封裝、移動大法,簡化條件邏輯是看得見的好處,起碼能解決短痛。

當然,簡化條件邏輯,本質還是在於封裝重構,將不同的分支獨立成一個函數,執行不同的邏輯業務,這一點才是簡化的重點。

第11章 重構API

  待補充

第12章 處理繼承關係

  待補充