寫不出這種程式碼,就等著被leader開除吧!
前言
在我們平時刷題的時候,你可能會寫過很多諸如
int a,b,c
int [] arrays=new int arrays[10];
if((numbers > 10 && flag == 'true') || flag =='false')
這種程式碼,對於我們自己練習程式設計或者解決一個演算法題,當然沒有問題。但是如果是在一個工程中,尤其是幾十上百人維護了幾年的工程中,還使用這種寫法,傾瀉自己天馬行空的才華,保證leader不打死你哦。
所以,對於程式碼的整潔性,可讀性,自古以來就有很多大神做出過總結,比如這本《clean code》,中文名叫做《程式碼整潔之道》,今天,我們就來看看吧。
命名
命名思想
首先就是命名,命名可以說是一切程式的基礎,如果用三個字來形容那就是——“有意義”。
你要做到,當一個人看到你的命名,就知道這個變數/函式是幹什麼的。
來看這一段程式碼:
public List<int[]> getThem() {
List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
if (x[0] == 4)
list1.add(x);
return list1;
}
這段程式碼非常簡潔,但是非常模糊,我們不知道theList到底是什麼,為什麼x==4作為判斷,list1又是什麼?
現在我們來改一改:
public List<int[]> getFlaggedCells() {
List<int[]> flaggedCells = new ArrayList<int[]>();
for (int[] cell : gameBoard)
if (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
從上面的程式碼中,我們可以馬上看出來,flaggedCells表達的是標緻cell,而cell位於gameBoard中,就是一個遊戲面板,只需要再通過檔名,知道這是一個【掃雷遊戲】,那麼cell就是每一個格子,if語句中就是判斷每一個格子是否被點選過,如果是,就新增到flaggedCells中,我們顯然知道他想要幹什麼——蒐集玩家點選過的格子並返回。
但是,上面的程式碼嗎使用的是 int 陣列,比如int[] cell,每一個數組內部的數表示cell的狀態,可是cell並不需要那麼多狀態,而且這樣導致每次使用這個狀態的時候都要重新定義陣列,狀態是cell的一個屬性,所以,完全可以定義一個cell的類,將他的狀態封裝進入。
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<Cell>();
for (Cell cell : gameBoard)
if (cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}
這樣子很清晰看出,gameBoard由Cell組成,從gameBoard中取出標記過的cell放入flaggedCells中,這樣的程式碼是不是感覺渾然天成,自然而然呢?
命名規範
通過以上事例,你應該理解在寫工程專案時,諸如陣列,連結串列,字典這些底層結構應該要封裝在User,Cell,Address這種類中,使用的時候直接使用這些類即可,這是一種大局觀的思維,現在我們來做一些較為細節的落地規範。對於什麼駝峰命名,匈牙利命名我相信你不會陌生,但是我在這裡再強調兩個地方。
-
類名
- 類作為一個物件,需要的是名詞或者名詞短語,gameBoard,Ueser都是如此,不要使用模糊名詞(就是概念很大的名詞),比如Data,因為可以細分為UserData,MoneyData等,對於可以細分的模糊名詞,一定要用名詞短語。
- 類名都是第一個字母大寫的名詞組合。
-
方法名
- 方法作為具體幹事的執行者,當然是使用動詞或者動詞短語了,同樣注意的是,不要使用模糊動詞,和上面不一樣的是,解決模糊名詞的方法是增加名詞修飾,而解決模糊動詞的方法是【換更精準的動詞】
- 比如getInformation,就不好,因為get太大了,你的information是pull過來的還是clone過來的?是被動接受的還是主動去取的?Information這個詞也很模糊,所以可以根據情況拆分為直接取使用者資料——fetchUserData,上傳新資料再取使用者資料——uploadAndfetchUserData。
- 不是不能用get,是說如果有更好的選擇,儘量用更精準的動詞。
- 注意方法名第一個字母小寫。
函式
第一原則
短小
如果還有第二原則,那還是短小。
短小到什麼程度,最多20~30行吧,
所以要求,一個函式,只做一件事情,
什麼叫做只做一件事情呢?
就拿處理資料來說,我們說【處理使用者資料】是一件事,你也可以說是做了三件事:
- 取資料
- 處理資料
- 返回資料
當然,這個例子有點抬槓的意思,不過反映的現實是,程式碼中各種邏輯往往你中有我,我中有你,到底一件事情的邊界在哪裡?
這個因人而異,我只能提出書中的方法。
-
同一抽象級
剛才那個取資料,就是在同一抽象級下完成的事情,就可以看做是一件事,如果再來一件——儲存使用者資料,顯然,可以歸到前面,還是一件事,如果再來一件——取車輛資料,顯然,就是另外一個抽象的東西了。
-
分割判定與處理
這個好理解,就是立法與司法的分割,比如:
if (set("username", "unclebob")) ...
就讓人迷惑,他表達的意思是不是如果unclebob成功賦值給username就返回true呢?還是說如果username為null時就用unclebob
賦值進入呢?username到底是屬性還是碰巧一個字串叫做username呢?隨便猜。
但是我們這樣修改:
if (attributeExists("username")) { setAttribute("username", "unclebob"); ... }
如果有username這個屬性,就賦值,非常清晰,也就是說,判定與處理要分隔開。
你可能會說,對於像if,while,switch這種語句,往往動輒十幾行,短不了啊!
首先,這個原則不是鐵律,實在太長也沒辦法。
其次,對於這些語句,完全可以講裡面的邏輯做一個封裝,比如這種:
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
if 語句之後做了一件includeSetupAndTeardownPages的事情,不僅極大增強了可讀性,而且程式碼短了不少。
關於引數
當你看jdk原始碼或者是android原始碼的時候,你會發現他們經常做呼叫,尤其是同名的方法過載,在看《演算法》這本書的時候,也是,比如關於快速排序的:
public static void quickSort(int [] arrays){
quickSort(arrays,0,arrays.length-1);
}
private static void quickSort(int [] arrays,int left,int right){
...
}
我在一開始接觸的時候,覺得雖然好看,但未免麻煩,隨著經驗的提升,這是一種非常好的程式設計習慣。
首先,對於使用者來說,他想要進行快排,想傳的只有陣列,左右邊界都包含進去了,你為什麼要他多傳引數?同時第一個方法使用的是public,就表示這是暴露給使用者的。
其次,第二個方法在第一個方法中被呼叫,用private保護了起來,避免了無數麻煩。
所以,越是業務層的邏輯,越要寫引數少的程式碼,如果引數必須要很多,那就用一個private的函式封裝起來,你讓使用者擁有一百個引數輸入,對他來說,那不是自由,那是災難。
蘋果和微信的使用體驗就是將這種哲學貫徹到極致的代表。
註釋
最好的註釋,就是程式碼本身。
用程式碼能解釋清楚的事情,儘量少用註釋,如果註釋太多,只能證明程式碼寫得爛……
不過有些地方還是有必要寫程式碼的。
位置
最好寫在方法頂部,不插入到實際程式碼中,比如那個面試幾乎必問的String中的equals方法,原始碼如下:
...
* @see #compareTo(String)
* @see #equalsIgnoreCase(String)
*/
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = length();
if (n == anotherString.length()) {
int i = 0;
while (n-- != 0) {
if (charAt(i) != anotherString.charAt(i))
return false;
i++;
}
return true;
}
}
return false;
}
警示的註釋
這裡可以寫一些告誡他人的程式碼,讓後來的接盤俠能夠引起重視。
//When I wrote this, only God and I understood what I was doing
//Now, God only knows
簡單說就是,下面的程式碼一定有用,但是我也看不懂了,你別碰O(∩_∩)O哈哈~
在知乎上看到還有這樣的。
//如果這段報錯,你在機器上裝一個360安全衛士,相信我我
以為是開玩笑,結果裝了就真的好了。這個是前人留給我的。
//這個服務有問題的話,你可以問某某某,這段是他寫的。
這個是在我離職交接時寫的,出賣了未離職的一個同事。
//執行成功後傳送一條通知簡訊,穩定後註釋掉
手機號是寫死的,我看到這段的時候還沒有註釋,這一年每天凌晨他都能收到簡訊。
作者:hll
連結:https://www.zhihu.com/question/296123587/answer/498701733
來源:知乎
這些人水平怎麼樣另說,但是對於後人還是用心的。
TODO註釋
這個就不說了,很常用。
看完了有用的註釋,我們來看看沒用的註釋
多餘的
// Utility method that returns when this.closed is true. Throws an exception
// if the timeout is reached.
public synchronized void waitForClose(final long timeoutMillis)
throws Exception
{
if(!closed)
{
wait(timeoutMillis);
if(!closed)
throw new Exception("MockResponseSender could not be closed");
}
}
這就是把程式碼做了什麼又描述一遍,沒有任何意義。
被註釋掉的程式碼
InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(), formatter.getByteCount());
// InputStream resultsStream = formatter.getResultStream();
// StreamReader reader = new StreamReader(resultsStream);
// response.setContent(reader.read(formatter.getByteCount()));
相信很多人都有這樣的行為,在自己寫演算法題的時候用來測一測沒有任何問題,但是對於後來者來說,他該怎麼辦?
他一定會想:也許是有用的呢?不然為什麼之前要寫。
但是他又看不懂或者覺得沒必要看,於是就留了下,然後這樣的程式碼就會越來越多,最終成為傳說中的祖傳程式碼。
不用心
註釋要清楚,如果註釋還要寫註釋來解釋,根本沒有意義。
比如下面這個,為什麼要用200?
/*
* start with an array that is big enough to hold all the pixels
* (plus filter bytes), and an extra 200 bytes for header info
*/
this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];
小結
對於多數人來說,命名函式與註釋基本上就是程式的主要組成部分,能夠處理好這三樣就能寫出非常好的程式碼了,當leader看到你的提交的時候,看到的是如此優雅的程式碼,我想,他也會覺得是一種享受吧,就和詩歌一樣。
作者簡介:小松漫步,一個剛入職的新人,微信公眾號【小松漫步】,文章參考自《程式碼整潔之道》,公眾號回覆【程式碼整潔之道】即可獲取資源,一起加油吧。