程式碼都寫不完,還寫個錘子註釋!

四猿外發表於2021-10-21

現在的專案開發裡,程式碼註釋就像程式設計師的頭髮,越來越少。

尤其是國內,這種現象不僅是在小公司小團隊中司空見慣,就算在大公司,以及大團隊中的開源專案裡,也是屢見不鮮。

上圖是我在阿里的 Druid 專案原始碼裡截的。DruidDataSource 是 Druid 重度使用的核心類,非常關鍵,可是哪怕這種關鍵的核心類,也見不到什麼註釋。

這張圖則來自阿里的另一個著名開源專案Dubbo。DubboProtocol 是 dubbo 協議的實現類,而 dubbo 協議是 Dubbo 專案中最常見,使用最頻繁的預設協議,一樣沒什麼註釋。

沒有註釋對我們讀程式碼帶來了很多的不便之處。就像扔給你一個數碼產品,上面堆疊著密密麻麻的功能按鍵,但是卻沒有給你說明書。

那為什麼程式碼註釋消失了呢?

我嘗試總結一下原因:

1. 國內程式設計師的職業環境對加註釋不友好

在國內這種環境裡,程式設計師們每天在苦悶的 996 中掙扎,各種大活小活不斷地做著,正常寫程式碼都忙得不可開交,加註釋更是進一步提升了工作量,沒人喜歡自己給自己加工作量的。

我們們想想,在費勁巴拉地寫完一大堆程式碼之後,經過反覆自測修改之後,好不容易調通了,腦子已經暈乎乎的了,你此時會有多大心思去寫這段註釋呢?

又再想想,你可能想著要給程式碼加註釋呢,突然這邊產品拉你開會,又或者那邊運營告訴你,需求變了,剛寫好的程式碼還得再改改……此時,你還有給程式碼加註釋的念頭嗎?

另外,註釋這事兒,寫好了是很費精力的。一般來說,一段好的註釋,要能在有限的行數之內說明出:被它註釋的程式碼到底做了什麼,是個怎樣的概念以及為什麼會寫這段程式碼

寫註釋麻煩不說,關鍵是註釋還不算我們們程式設計師的工作量。

程式設計師的工作是把業務用程式實現,工作結果裡不看你註釋了多少程式碼,也不看你註釋寫的好還是壞,只看你的程式是不是寫完了,滿足了需求沒有,會不會上線出什麼問題。

至於註釋,它滾出了程式設計師兄弟們的 KPI。有多少公司能像 Google 那樣去 Review 程式碼的?BAT 有一個算一個,都差點意思。

所以,國內程式設計師們糟糕的環境,是程式碼註釋少的首要原因。

2. 看待註釋的方式出現了變化

Java 是一門物件導向的語言,從它出世以來,業界就不斷地為 Java 制定了數不清的規範。

在 2008 年,集這些規範於大成的《Clean Code》—— 中文名叫《程式碼整潔之道》這本書出現了。

在《程式碼整潔之道》中有個理念就是,註釋是為了彌補程式碼表達能力不足的一種不得已的做法。如果程式碼能表達清楚,那就沒必要寫註釋。

甚至,這本書的作者認為寫註釋都需要用 failure 這個詞來形容,也就是說,如果你寫了註釋,那就說明你的程式碼不夠好,你寫好程式碼的努力失敗了。

這個理念在業界也被不少大牛們認可了。所以,後面就有越來越多的人認為:程式碼寫的夠好,就不用寫註釋了。

如果大家有空,可以去看看《程式碼整潔之道》的第四章,裡面詳細說明了這種如今被業界不少人接受的關於註釋的理念。

所以,“好程式碼不需要註釋”這種觀點也是造成註釋少的一個原因。

3. 註釋沒有規範,導致質量參差不齊

很多團隊裡,是沒有註釋規範的。對怎麼註釋,在哪裡註釋沒有任何規定,隨意程式設計師們自由發揮。

這就麻煩了,註釋一旦寫了,它就很關鍵了。因為

錯誤的註釋,比沒寫註釋還禍害人

註釋寫的很差,那不僅沒起到註釋本應該起到幫助讀程式碼的作用,反而還可能影響讀程式碼,甚至還能把人帶坑裡。

如果沒有註釋規範,往往經常就會出現有人做的好有人做的差的情況。

比如,有人到處加註釋,i = i + 1; //把i加1連這種簡單程式碼都恨不得加註釋,這就有點脫褲子放屁了。

還有的人寫註釋了,但是需求變了,程式碼改了之後,註釋懶得改了。又或者是改程式碼的人不是原作者,新人改完之後壓根就沒意識到要改註釋。

所以,如果沒有規範,很多程式設計師對註釋沒有什麼正確的概念,沒寫好註釋由此還引來了埋怨……久而久之,就沒人愛幹加註釋這件事了。

到底應該怎麼寫註釋呢?

談了那麼多不寫註釋的原因,這裡也想說明一下我對註釋的觀點。

我個人並不怎麼贊同《程式碼整潔之道》對註釋的觀點,我自己讀有好註釋的程式碼,直接就省了五成以上的力氣。有好註釋的程式碼讀起來,就像常年腦血栓,一朝被皮搋子打通了一樣,那叫一個順暢。

比如,看看 Netty 的註釋:

/**
 * A nexus to a network socket or a component which is capable of I/O
 * operations such as read, write, connect, and bind.
 * <p>
 * A channel provides a user:
 * <ul>
 * <li>the current state of the channel (e.g. is it open? is it connected?),</li>
 * <li>the {@linkplain ChannelConfig configuration parameters} of the channel (e.g. receive buffer size),</li>
 * <li>the I/O operations that the channel supports (e.g. read, write, connect, and bind), and</li>
 * <li>the {@link ChannelPipeline} which handles all I/O events and requests
 *     associated with the channel.</li>
 * </ul>
 *
 * <h3>All I/O operations are asynchronous.</h3>
 * <p>
 * All I/O operations in Netty are asynchronous.  It means any I/O calls will
 * return immediately with no guarantee that the requested I/O operation has
 * been completed at the end of the call.  Instead, you will be returned with
 * a {@link ChannelFuture} instance which will notify you when the requested I/O
 * operation has succeeded, failed, or canceled.
 *
 * <h3>Channels are hierarchical</h3>
 * <p>
 * A {@link Channel} can have a {@linkplain #parent() parent} depending on
 * how it was created.  For instance, a {@link SocketChannel}, that was accepted
 * by {@link ServerSocketChannel}, will return the {@link ServerSocketChannel}
 * as its parent on {@link #parent()}.
 * <p>
 * The semantics of the hierarchical structure depends on the transport
 * implementation where the {@link Channel} belongs to.  For example, you could
 * write a new {@link Channel} implementation that creates the sub-channels that
 * share one socket connection, as <a href="http://beepcore.org/">BEEP</a> and
 * <a href="http://en.wikipedia.org/wiki/Secure_Shell">SSH</a> do.
 *
 * <h3>Downcast to access transport-specific operations</h3>
 * <p>
 * Some transports exposes additional operations that is specific to the
 * transport.  Down-cast the {@link Channel} to sub-type to invoke such
 * operations.  For example, with the old I/O datagram transport, multicast
 * join / leave operations are provided by {@link DatagramChannel}.
 *
 * <h3>Release resources</h3>
 * <p>
 * It is important to call {@link #close()} or {@link #close(ChannelPromise)} to release all
 * resources once you are done with the {@link Channel}. This ensures all resources are
 * released in a proper way, i.e. filehandles.
 */
public interface Channel extends AttributeMap, Comparable<Channel> {

Channel 是 Netty 裡非常核心的一個介面,你直接看註釋,一下子就能理解了 Netty 為啥搞出個 Channel 類來,Channel 類你可以怎麼玩兒,這些 Netty 在註釋給你說得清清楚楚、明明白白。

所以,我覺得註釋一定是要的,只是需要有個標準,也要有個度。

從實踐上看,我們團隊有這麼幾個必須加註釋的標準:

1. 複雜的業務邏輯

業務邏輯關聯太多的東西又或者步驟非常多,更或者兩者兼有,那麼就很少有人會去耐心仔細的去一行一行的把整個程式碼全部讀通理順。

這時候,必須在業務邏輯實現的相關類中,把類在業務邏輯實現中是個什麼成分,為什麼這麼設計類,以及對應的業務邏輯都要講清楚。並且重構程式碼後,註釋也必須跟著重構。

2. 晦澀的演算法

演算法也要加上註釋的,尤其那些深奧的演算法。大家不可能都是演算法專家,能一下子就通過程式碼理解到演算法實現的真諦。所以,這裡也要加上註釋,一般是說明這是用了個什麼演算法,這套演算法的出處或者附上相關文章的引用地址。

3. 非常規的寫法

非常規的寫法往往是有特殊情況,不得已為之的。比如,為了得到更好的效能;又比如,為了修復一個 bug,卻不想對程式碼進行大改動。

總之,非常規的寫法就是反模式、反套路的,有時候甚至會違反程式設計師的直覺。像這些做法,必須在註釋中寫明這樣實現的原因。

4. 可能有坑卻暫時沒太好解決辦法

有些時候,需求出的夠難夠複雜,時間上催的又很急,你根本沒辦法馬上想到特別好的辦法去實現。只能臨時想個簡單粗暴的方案,先湊活著。甚至還會在某些地方,把一些變數的值寫死先去把本期的需求實現了。

像這種就很可能就會給後面挖坑了。這時候,註釋必須加上為什麼要這麼解決的原因,還必須加上 //TODO 這類的,表示後面需要進行進一步的修改。

5. 關於專案核心的介面、類和欄位

做專案的時候,需求中的很多核心概念很可能會被對映到對應的介面或者實體類上,如果在這些核心介面和實體類加上清楚的註釋,寫明對應的業務概念,那麼,後面再維護專案的時候,真的是事半功倍。

比如,我們在一套批量排程系統裡,可能有多種任務的概念,有需要限定執行時間的任務,也有不需要限定執行時間的任務,那麼實現上,就可能有個 LimitedTimeTask 類對應限定時間的任務,還有個 UnLimitedTimeTask 類去對應不需要限定執行時間的任務。那這兩個類就必須加上註解,寫清楚對應的業務概念。

如果特定概念是複合的,是由多個小概念構成,卻必須用一個介面或者一個類來表示,那很可能實現上,就還得用欄位去對映這些小概念,那麼這些欄位也得加上註釋說明起對應的概念。

總之,註釋我個人理解必須要有,但是不可能太氾濫,必須有節制、有規範的加。

最後,我們們說白了,我對註釋的態度就是,和寫程式碼一樣,要有規範。

在這裡和管理者說一句,如果你希望大家寫好註釋,不能就靠一句“必須寫註釋”這麼高高在上的話去要求大家。沒有規範,你就不能完全怪程式設計師不加註釋了。

最最後,大劉提醒我“四哥,註釋規範裡還要再加一條”

對專案、領導的吐槽,可以寫到註釋裡!

認為大劉說的對的,可以點個贊


你好,我是四猿外。

一家上市公司的技術總監,管理的技術團隊一百餘人。

我從一名非計算機專業的畢業生,轉行到程式設計師,一路打拼,一路成長。

我會把自己的成長故事寫成文章,把枯燥的技術文章寫成故事。

歡迎關注我的公眾號,關注後可以領取高併發、演算法刷題筆記、計算機高分書單等學習資料。

相關文章