《代碼整潔之道》讀書筆記
- 2019 年 10 月 3 日
- 筆記
為了獲得更好的閱讀體驗,請訪問原文:傳送門
一、前言
代碼是什麼呢?或者說作為程序員的我們,對於寫代碼這件事又是抱着怎樣的一種態度呢?我時常都在想,如今我如願成為了一名程序員(雖然還很菜),寫代碼這件事成了我的工作,我期望從工作中獲得些什麼?而工作又能給予我什麼呢?
我在短暫的工作經歷中(4 個月),犯下過不少錯,少部分是因為經驗,但大部分的情況下都是因為對代碼沒有足夠的敬畏之心導致的,並且在工作中也遇到過一些很有意思的代碼,所以今天就着這本《代碼整潔之道》,來談一談對於代碼的感受和一些想法。(Ps:想吐槽一下這本書挺魔怔的..)
二、什麼是整潔的代碼
我搜索「代碼」這兩個關鍵字,給出的官方解釋都特別有意思,摘一下百度百科的好了:
代碼就是程序員用開發工具所支持的語言寫出來的源文件,是一組由字符、符號或信號碼元以離散形式表示信息的明確的規則體系。代碼設計的原則包括唯一確定性、標準化和通用性、可擴充性與穩定性、便於識別與記憶、力求短小與格式統一以及容易修改等。 源代碼是代碼的分支,某種意義上來說,源代碼相當於代碼。現代程序語言中,源代碼可以書籍或磁帶形式出現,但最為常用格式是文本文件,這種典型格式的目的是為了編譯出計算機程序。計算機源代碼最終目的是將人類可讀文本翻譯成為計算機可執行的二進制指令,這種過程叫編譯,它由通過編譯器完成。
好了,學術介紹一大堆,重點還是在最後一句:計算機源代碼最終目的是將人類可讀文本翻譯成為計算機可執行的二進制指令。
再精簡一下:「人類可讀」、「計算機可執行」。
說到底,代碼最終還是寫給人看的,所以「可讀性」就顯得尤為重要,但總歸我們是要先有「代碼」,再有「可讀的代碼」,經過不斷重構or重寫,最終形成我們「簡潔的代碼」。
說幾點感受比較大的吧。
方法盡量短 && 職責單一
有誰能告訴我下面這個方法究竟是在做什麼嗎?
/** * @author Administrator * */ public class GeneratePrimes { /** * @param maxValue is the generation limit. * */ public static int[] generatePrimes(int maxValue) { if (maxValue >= 2){ //the only valid case //ddeclarations int s = maxValue +1 ;// size of array boolean[] f = new boolean[s]; int i; //initialize array to true. for ( i = 0;i < s;i++) { f[i] = true; } f[0] = f[1] = false; // sieve int j; for (i = 2;i < Math.sqrt(s) + 1; i++) { if (f[i]) { // if i is uncrossed , cross its multiples. for (j = 2 * i; j < s ;j += i) { f[j] = false; //multiple is not prime } } } // how many primes are there? int count = 0; for (i = 0;i < s; i++) { if (f[i]) { count ++; //bump count. } } int[] primes = new int[count]; //move the primes into the result for (i = 0,j = 0;i < s;i++) { if (f[i]) { primes[j++] = i; } } return primes; } else { //maxValue < 2 return new int[0]; // return null array if bad input. } } }
如果你非常有耐心地看完了,你可能大概或許會了解到,這是一個返回 maxValue 範圍以內的質數的方法,但是我們經過簡單的重構之後,會變得更加容易理解:
public class PrimeGenerator { private static boolean[] crossedOut; private static int[] result; public static int[] generatePrimes(int maxValue) { if (maxValue < 2) { return new int[0]; } else { uncrossIntegersUpTo(maxValue); crossOutMultiples(); putUncrossedIntegersIntoResult(); return result; } } private static void putUncrossedIntegersIntoResult() { result = new int[numberOfUncrossedIntegers()]; for (int j = 0, i = 2; i < crossedOut.length; i++) { if (notCrossed(i)) { result[j++] = i; } } } private static int numberOfUncrossedIntegers() { int count = 0; for (int i = 2; i < crossedOut.length; i++) { if (notCrossed(i)) { count++; } } return count; } private static void crossOutMultiples() { int limit = determinuIterationLimit(); for (int i = 2;i <= limit; i++) { if (notCrossed(i)) { crossOutMultiplesOf(i); } } } private static void crossOutMultiplesOf(int i) { for (int multiple = 2 * i; multiple < crossedOut.length; multiple +=i) { crossedOut[multiple] = true; } } private static boolean notCrossed(int i) { return crossedOut[i] == false; } private static int determinuIterationLimit() { double iterationLimit = Math.sqrt(crossedOut.length); return (int)iterationLimit; } private static void uncrossIntegersUpTo(int maxValue) { crossedOut = new boolean[maxValue+1]; for (int i = 2; i < crossedOut.length ; i++) { crossedOut[i] = false; } } }
首先我們通過私有方法隱藏掉了實現的具體細節,並且使用有意義的命名,使得我們主函數 generatePrimes
更加便於理解。
函數的第一規則就是要短小,第二條規則就是要更短小。每個函數保持職責單一,並且有意識的維持在一定行數內(JVM 就強制要求每個函數要小於 8000 行…也聽過每個函數盡量維持在 15 行 or 30 行 之內這樣的說法..可能有點魔怔,但要點就是函數要盡量短小),這當然是最理想的情況,而現實的情況往往要糟糕一些。
在工作中,我就遇到過一些長得可怕的方法,他們或許本來保持着單純,職責單一,但是經過業務不斷的改造,需求不斷的疊加,甚至是一些臨時邏輯的加入,這個方法就變得越來越臃腫不堪…並且因為業務的不斷發展,越來越少的人會 care 到它,以至於改造成本越來越大,甚至被遺忘在角落..
這其實是再正常不過的事情,但在多人協作的項目中,有一點需要自己來維持清醒,那就是:「一個方法就可以返回的為什麼要寫兩個?」,關於這一點,保持自己的思考就好了..
注釋要體現代碼之外的東西
有一句聽起來好厲害的話叫做:「代碼即注釋」,不知道大家是怎麼看待這樣一句話的,或者說是怎麼看待注釋的。其實反過來想,如果你的代碼需要大量的注釋來解釋其中的邏輯,會不會是代碼本身就存在一定問題?或者換個角度思考,注釋是用來解釋代碼邏輯的嗎?
可怕的廢話
我們來看下面這一段代碼的注釋:
/** The name. */ private String name; /** The version. */ private String version; /** The licenceName. */ private String licenceName; /** The version. */ private String info;
上面這些 Javadoc 的目的是什麼?答案是:無。並且仔細閱讀,你甚至會發現一處剪切-粘貼導致的錯誤,如果作者在寫(或粘貼)注釋時都沒有花心思,怎麼能指望讀者從中收益呢?
能用函數或變量時就別用注釋
看看以下代碼概要:
// does the module from the globale list <mod> depend on the // subsystem we are part of? if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem())
可以修改成以下沒有注釋的版本:
ArrayList moduleDependes = smodule.getDependSubsystems(); String ourSubSystem = subSysMod.getSubSystem(); if (moduleDependes.contains(ourSubSystem))
用代碼來闡述
有時,代碼本身不足以解釋其行為。但不幸的是,許多程序員以此為由,覺得大部分時候代碼都不足以解釋工作。這種觀點純屬錯誤,比如你願意看到下面這個:
// Check to see if the employee is eligible for full benefits if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))
還是這個:
if (employee.isEligibleForFullBenefits())
只需要多思考那麼幾秒鐘,就能用代碼解釋你的大部分意圖。其實很多時候,簡單到只需要創建一個描述與注釋所言同一事物的函數即可。
小結
注釋終歸是要用來體現代碼之外的東西..
名副其實的名字
取名字這件事,真的是程序員的一門藝術。腦海裏面能浮現出同事們用翻譯軟件取名的畫面嗎?
一個好的名字再怎麼誇讚都不為過,但是這個最基礎的前提就是,它首先得是一個「正確的名字」。我就遇到過一次,函數名字叫做類似於 listAll
這樣的東西,戳進去看實際上還基於業務規則做了過濾..(這樣的牛肉不對馬嘴的情況又讓我聯想到了注釋這樣的東西,可能實際的代碼已經作了更改,但是注釋還是是維持原樣沒有變化..)
並且還有一個特別有意思的點,就是關於名字的「長度」。有時候可能為了想要描述清楚一個變量 or 一個類的具體作用,我會給它起一個看起來特別長的名字..關於這個,這裡有一些小經驗可以分享一下:
- 去掉 Info 和 Data 這樣的後綴:這些就像是英語中的 a/ an/ the 一樣,是意義含糊的廢話,廢話都是冗餘的..
- 不要給變量加前綴來標識:變量不需要一個
m_
or 其他什麼的前綴來標識這是一個變量.. - 思考是否有必要標識出變量的類型:我們標註出變量的類型的目的是什麼?對於弱類型的語言,可能有時候還是必要的,因為我們有時候並不能從
students
這個變量中判明我應該怎樣對這個變量進行操作,但是對於 Java 這樣的強類型的語言,我們就需要根據實際的場景思考是否真有那麼必要了。
無副作用
函數承諾只做一件事,但還是會做其他被隱藏起來的事。
public class UserValidator { private Cryptographer cryptographer; public boolean checkPassword(String userName, String password) { User user = UserGateway.findByName(userName); if (user != User.NULL) { String codedPhrase = user.getPhraseEncodedByPassrod(); String phrase = cryptographer.decrypt(codedPhrase, password); if ("Vliad Passwordw".equals(phrase)) { Session.initialize(); return true; } } return false; } }
上面的函數副作用就在於對 Session.initialize()
的調用。checkPassword
函數,顧名思義就是用來檢查密碼的,該名稱並未暗示它會初始化該次會話。所以,當某個誤信了函數名的調用者想要檢查用戶有效性時,就得冒着抹除現有會話數據的風險。
所以這裡可以重命名函數為 checkPasswordAndInitializeSession
,雖然這還是違背了 "只做一件事" 的規則。
函數參數儘可能少
一個函數最理想的參數數量是 0,其次是 1,再次是 2.. 要避免使用三個以上參數的情況,因為參數帶有太多的概念性,參數過多就會帶來更多的複雜性..
我就見過一個查詢接口,為了滿足不同的複雜查詢場景,參數大概可能有接近 10 個.. 就算不為接手的編碼人員考慮,測試人員也會頭疼的.. 想想看,要覆蓋如此兼容如此多場景如此複雜的一個查詢接口,測試用例究竟應該怎麼寫呢?
More..
這本書說實話看下來挺魔怔的.. 裏面有許多簡潔實用的觀點可以讓我們受益,我僅僅挑了一些最近比較感同身受的幾點,來進行了說明。
代碼倉庫就像是一本《哈姆雷特》一樣,每個人都有自己不同的見解,這無可厚非,我覺得重要的就是要保持對代碼的敬畏之心,保持自身的思考,才能讓我們不斷向前(說話都變魔怔了..)
三、代碼之外
每個人都能寫出好的代碼
這就是一個非常有意思的話題了,我們可以分成幾個角度來思考:
- 好的代碼是寫出來的嗎?(這可能有點類似於好的文章是寫出來的嗎?)
- 為什麼我們寫不出好的代碼?
我記得之前在看《重構:改善既有代碼的設計》這本老經典的書的時候,就提到一種觀點說:「重構不是一個一蹴而就的事,需要長期的實踐和經驗才能夠完成得很好。重構強調的是 Be Better,那在此之前我們首先需要先動起手來搭建我們的系統,而不要一味地「完美主義」。」
好的代碼也是這樣,需要一個循序漸進的過程,雖然大部分時候,經驗可以讓我們少走許多彎路,但這些都是一個過程。
當然上面所說的全部,都是理想中的狀況,而現實中的情況往往不允許我們這樣做。什麼之前炒起來的 996,什麼 ICU,都無情的揭示着大部分程序員的現狀:忙。忙於各種已經堆成山的需求 && 修復各種 BUG 中。
我學到一個很正經的概念,叫做「管窺」,附帶的一種概念叫做「稀缺」。(看完下面這個故事應該很容易理解,故這裡不作解釋..)
我記得之前看過一篇報道,說是香港某富豪在節目中要體驗幾天環衛工人,參加節目前,他曾說過:「我的人生其實沒有很多時間坐下來,想想現在的生活不錯,享受一下。我有時間就會計划下一步!"
可幾天下來,讓他最糾結的竟然是吃飯問題,他對着鏡頭說:「很奇怪,我這兩天只是考慮吃東西,完全沒什麼盼望,什麼都不想。我努力工作,就是希望吃一頓好的。」
程序員是一種很容易陷入,對於時間「稀缺」狀態的物種。稀缺會俘獲我們的注意力,並帶來一點點好處:我們能夠在應對迫切需求時,做得更好。但從長遠的角度來看,我們的損失更大:我們會忽視其他需要關注的事項,在生活的其他方面變得不那麼有成效。(摘自《稀缺》P17)
這聽上去就像是在找借口一樣,但其實有點差別。我發覺每個人其實都能夠寫出好的代碼,只是取決於你有沒有這樣的意識,有沒有堅持自己的思考,更重要的是,有沒有「跳出需求」,甚至是「跳出工作」之外來思考,就像是要跳出「我們明明知道了很多道理,卻依然過不好這一生」的怪圈一樣。
結尾
這一段時間都不怎麼更新了,不是我變懶了.. 前段時間就陷入了不加班就完成不了工作的狀態,一方面是因為事情比較雜.. 另外一方面就是自己效率還不夠高.. (悄悄說:雖然很忙,但是總是能抽得出時間玩兒手機 hhhh…)值得反思吧.. 最近也開始有一些覺得越來越難下筆了.. 想寫的東西很多,但總怕寫不好..
另外,程序員真的是很有意思的職位了,並且也覺得程序員都多少帶着點兒自己的驕傲來得,因為每天都在自己的世界玩兒拼圖,自己就是世界的造物主,久了,難免有些受影響..(主要體現在溝通上..)
摁.. 總之這是一本很好的書,建議感興趣的童鞋可以溜一遍。
按照慣例黏一個尾巴:
歡迎轉載,轉載請註明出處!
獨立域名博客:wmyskxz.com
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關注公眾微信號:wmyskxz
分享自己的學習 & 學習資料 & 生活
想要交流的朋友也可以加qq群:3382693
錢