通過文檔理解代碼的時代已經過去了?

前幾天和領導(算得上是技術第一梯隊的)對話,大致是說這個項目極度混亂:第三方拖沓不辦事,項目代碼和資料庫以及存儲過程無文檔無注釋無可讀性,項目的負責人多次改動等等。期間談到無文檔無注釋無可讀性的時候,領導竟然說了一句「通過文檔理解代碼的時已經過去了,你多看看代碼,跑一跑,追進去看看來理解項目」。當時心中是有些震驚的,因為這位領導在技術方面說話還是比較有分量的!

所以,難道真的是這樣?


基本上的原則就是把code寫成詩;文檔就在code里, code as doc和 test as living doc

努力去學習,大量的練習,思考,把一段代碼寫完回頭重複的自我review,直到你再也沒辦法讓它更簡單更達意為止;

當你熟悉了這種感覺,寫出漂亮的code就會變成呼吸一樣自然的事情。(源自處女座老婆對於我「天天把家裡的東西嚴格的整理成這樣,你就不累不煩么?」問題的回答,具體回答請自行腦補)

-----------------------------------------

原回答:這說的是把code寫的像文檔一樣漂亮,把代碼組織的像文檔一樣結構清晰,層層遞進,娓娓道來的意思,而不是懶省事可以不寫文檔的託詞。別搞錯了。

盡量把能寫進代碼(是真的代碼而不是注釋)的文檔以代碼的形式寫進去,這個追求還是要有的。

看了一些答案,有些小盆友似乎完全不知道或者認為不可能「把code組織的像文檔一樣」的態度,這讓我想起了「中美計算機是不是差了50年」那個問題…

升Sr. 就不用寫code了,轉manager就不用寫code了,這樣的developer可能也沒有「把代碼寫成詩」的情懷吧…

差在技術,我們可以努力去追,差在態度嘛… 祝大家「都」早日晉陞管理層吧 ( ^ω^ )

========================更新分割線=====================

把自己個人在公司wiki里的code review check point 分享在最後了(看中文往最後拉)

Principles

Readability

「…we want to establish the idea that a computer language is not just a way of getting a computer to perform operations but rather that it is a novel formal medium for expressing ideas about methodology. Thus, programs must be written for people to read, and only incidentally for machines to execute….」 —— 《SICP》

"...One step more, the program should be written for people to talk, as PM/TPM/SDM/Stakeholder do not read your code, the program should follow the ubiquitous languagein DDD, and can be talked between SDE/PM/TPM/SDM/StakeHolder, so that it can be tested by business guys during they talk, think, work, and evolve together with business... " —— 阿萊克西斯

Common Code Review Check Point

  • !!!!!!GIVE GOOD NAME TO YOUR VAR AND FUNCTION!!!!!

    To reflect the domain knowledge/concept you obtained during discussion with PM/customer; Make your domain model screaming in your code, notice every reader about how your code is in sync with the concept model in PM/Customer"s mind.
  • Avoid if-else or branch nightmare, by only

    checking the condition path you care about, instead of checking each possible branch and react redundantly .

    //!!!!!!!!!!!!!!branch nightmare!!!!!!!!!!!!!!

    if (a!=null) { if (a.b==null) {handOtherCase() } else { if (a.b.c&>100) { // normal case here
  • } else {

    handOtherCase() } } } else {

    handOtherCase() } /*

    * the if-else should be designed for different actions you care about in branches, instead of how many total branch there may exist,

    * in this example, we only care about 2 scenarios, 1) when c in b in a larger than 100, 2) all other case;

    * so no matter how many branches there may exist, we should only write one if else to explicitly highlight the fact and Let the code scream!

    * !!!!!!!!!!!!!!!!!!!!!!!!!WE ONLY CARE ABOUT THOSE 2 SCENARIOS !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

    * so the code can be refactor to

    */ if (a!=null a.b!=null a.b.c&>100) { //do normal case

    return; }

    handOtherCase();
  • Does your Code enable clean/readable test to test all branches of your code? have you ensure all your business logic/branch is tested? hard to test code is bad code.
  • Global variable is forbidden (non-final public static variables), global constant is allowed
  • ZeroConfig: whenever possible, calculate your config base on env data (e.g. do not put /apollo/env/{YourApolloEnv} in brazil config, use ApolloEnvironmentInfo

    to obtain it in runtime), or use default value for where you want to set

    config, see Configulation
  • Rely on explicit knowledge, not implicit knowledge:

    To check/read your code make sense or not, should rely on the knowledge

    telling by your current code, instead of relying on implicit knowledge

    only you know. e.g. this Set& appear to be changed

    everytime we accept one request, but I know all our request shall put

    content NOT out of scope when this Set initialized, so actually this Set

    is immutable, and my remaining code is relying on that fact; this

    violate the principle here. This principle can also be understood as rely on what you see, not what you know. This is the key for building easy to reason code/system
  • Class level static variable is forbidden(non-final private/protected static variables), unless you have STRONG reason to do so, use case should be reviewed by senior team members. class level constant is allowed.
  • Parameter/Field design correctness: A variable/dependency should be a function

    parameter or class field(injected by constructor)?
    should be

    decided by who should provide that dependency, it"s the bootstrap

    program which wire everything together charge to decide that (then it

    should be field of class and injected by constructor), or it"s the

    runtime user who relies on the functionality provided by your class

    should decide that (then it"s should be function parameter). Another way

    to check is: when you design something as function parameter, does all

    your client pass the parameter with same value? if YES, then it"s

    indicating this parameter should be the class"s field and decided only

    once during class construction.
  • Eliminate object relationship/dependency as much as possible,

    make object/class dependency as simple as possible; organize object

    graph to a simple tree is recommended, single direction is recommended

    than 2-way direction dependency(A knows B, and B also knows A). Use

    multiple dot to access something is coupling the object dependencies to

    your current class, do that with great care as that may indicate leaking

    knowledge to class, which shouldn"t know the knowledge(e.g.

    a.getB().getC().getE().doSomeCalculation(). this call leak knowledge

    of B has C has E to the calling code. while create a helper function in

    A, to wrap the call chain, can at least hide object relation to the

    calling code)
  • A class should have both constructor and

    function(for others to use), a class only have constructor, means use of

    the functionality and construct/prepare the functionality is coupling

    together.
  • NEVER return result, by modify one of

    your parameter. e.g. passing in empty list, and put your calculation

    result into that list, and return void; return your calculation result

    instead!!
  • Obey CQS(command query separation)

    principle, function has side effect(change state, change content of

    collection, write to file, write DB...etc) should be design to return

    void to highlight it cause side effect, and function return result

    should has no side effect, which means no matter how many time client

    call it, it should return same thing, and it should do no impact to

    other part of the system. Think twice/three/four times before you want

    to violate the principle.
  • Avoid nested {}, by check invalid case

    first, throw exception or return error code or default value, and

    process normal business logic after all check passed.

    //nested style

    if (...) { if (...) { if (...) { // normal case here

    } else { return null...// invalid case 1.

    } } else { throw exception...//invalid case2

    } } else { return....//invalid case3

    } //recommended style

    if (...) { //invalid case1

    } if (...) { //invalid case2

    } if (...) { //invalid case2

    } //normal case here

    // or put all invalid check into a dedicate validation function

    valid(....) //normal case here

  • Class should be stateless, unless you have Strong reason to have state in your class
  • Field of class should be final, try to refactor mutable field to method

    parameter if possible, include mutable field unless you have Strong reason to design your class to have state
  • ALL Dependencies(other business logic and; or service call/file system

    access) should be injected through constructor, or use them as function

    parameter. Depending on who construct the class, or who use the function

    of the class, should charging for deciding this class"s dependency
  • Use getInstance() to provide default way to construct your Class, if your class have many client use it in default way.
  • Use composition and avoid using inheritance, talk to Senior member in team to review inheritance if you do want to use it. See blow, or talk to Sr. member in team to understand why.

...inheritance is not an unmixed blessing. It implies that a component strongly

depends on the components it inherits from. This dependency can be

difficult to manage. Much of the literature on object-oriented design,

e.g., on design patterns [58], focuses on the correct use of

inheritance. Although component composition is less flexible than

inheritance, it is much simpler to use. We recommend to use it whenever

possible and to use inheritance only when composition is insufficient...

————

&<&Programming&>&>

「...The language does not prevent you from deeply nesting classes, but good taste should.

[...] Nesting more than two levels invites a readability disaster and

should probably never be attempted...」

———— &<&&>, Ken Arnold and James Gosling (1998)

  • Function should not contain too many nested {}, no matter it introduced by if-else or while/for
  • Use Stream API as much as possible to replace "for loop".
  • Define var near where you want to use it, do not define a var, then reader find out its usage only several lines later.
  • Avoid use static function, unless the function is simple, and you are sure it

    never need to change and you never need to replace it to another

    implementation; a good example of "never change" is Math.abs(); the

    behavior of that function is by definition of math, should never change,

    even in UT.
  • Avoid use {} to implement complex lambda expression, like { statement1; statement2; .....statement20}, all lambda expression should be small and simply to inline like x-&> doSomething(x); for situation where we need x-&> { statement1; statement2; .....statement20}; please refactor to put those { statement1; statement2; .....statement20} into one dedicate function, and give it an intention revealing name, and do x-&> intentionReavealingFunctionName(x)
  • Keep DI in code is also a choice, we can use the 「environmental」 parameter pattern, check:DI in functional programming?

把自己個人在公司wiki里的code review check point 分享在最後了,歡迎檢閱。有人看的話, 可以考慮翻譯成中文放在前邊。。。。

========================更新分割線=====================

簡單說下怎麼寫好code,想到哪寫到哪:

?用類名 方法名 參數名 變數名 當注釋,寫不下說明類 方法 參數 變數違反了 single responsibility principle, 把責任拆開,增加類 方法 來把事情講清楚;

?把隱藏在code的目的,概念用類名,方法名顯式表達。(最簡單的例子,if 條件里有5個檢查,可能其中三個檢查單獨表達的是一個業務規則,把他們提出來用函數名顯式表達這個業務規則的名字)

?用validation邏輯和well named exception來限制輸入/檢測異常狀態(而不是用注釋說明這邊輸入,現在的系統狀態不可能是XXX,所以這個YYY邏輯work)。

?嚴格遵守CQS, 盡量遵守盡量single responsibility principle;

?盡量保持business/tech logic separation,保持domain model的純潔性,讓domain model浮現;

?盡量消除程序狀態,慎用繼承;

?用支持BDD style(expect:when:then)的test framework寫UT,在UT里用真實事例勾勒程序的脈絡,把UT寫成詩!

?把類組織成樹, 而不是網,保持單向依賴,保持簡單深度或廣度遍歷code的可能性與簡單性,盡量保證code寫出來之後,讀的人不用走回頭路,不用跳來跳去去理解「一件事情」;

?去理解「程序%90的意義是給人讀的,%10的意義才是在機器上跑的」這句話的意思。

文檔啊,它還真就在code里!


寫了介面不寫注釋的行為都是耍流氓,別和我說什麼優雅的代碼就是注釋。我只想調個方法獲取一個值,我不想看你所謂的優雅。。。

搞不懂一堆邏輯在那裡,一句注釋就能說得清的事情幹嘛非要我去看代碼實現,迭代很忙的好不

--------更新

贊同 @鋼的琴 的說法,注釋也不能隨意亂寫,我把他答案中的API介面換成了介面,我認為介面是屏蔽實現高度抽象存在的,調用方不關心或者壓根看不到實現,此時沒有一個很好的注釋很難保證自己方法的意圖和調用方的意圖一致.不過對於findById(id)這種通俗易懂的那就沒必要了.

* 介面

* 複雜難懂的演算法

* 需要後來的維護者特別注意的地方

對於團隊來說代碼的可維護性始終放在第一位.另外我個人的觀點代碼分為業型性代碼,基建型代碼,對於前者要寫的大眾化,對於後者才是你炫技的平台.

我是寫業務的,因此我的代碼基本是下面這種風格,我倒覺得通俗易懂,不給他人留坑.

// 基礎型校驗

xxxxx

//獲取活動

xxxx

//獲取活動對應的任務

xxxx

//獲取活動對應的獎品

xxxx

//組裝VO

xxxx

//對於登錄用戶擴展VO

xxxx

return ResultWrapper


我們現在的產品有十來個代碼庫,維護了3年半,注釋幾乎為0 。

關於文檔注釋的問題,我在其他回答里說過

1,複雜的項目中,讓開發人員維護代碼的同時同步更新文檔和注釋幾乎是不可能的。文檔注釋隨時間會越來越與代碼脫節,不但起不到作用,反而干擾閱讀

2,高層設計,架構設計類確實需要文檔,這類文檔過時慢(同樣會過時),且對理解整個系統有很大幫助

3,代碼要注重命名,通過精確命名來提升代碼可讀性。這樣不但解決了注釋的問題,對編程能力的提升也有幫助

4,建議只在下述情況下寫注釋:

* API介面

* 複雜難懂的演算法

* 需要後來的維護者特別注意的地方


看到好多人說代碼注釋自動生成文檔,可你們忽略了複雜的業務,真正需要文檔說明並不是API,並不是這個函數這個方法在幹啥。

而是這個系統在做什麼,有什麼功能,這些功能涉及到哪些類,哪些類哪些庫用於做什麼的。

有了這些文檔,可以在幾小時內大致了解整個系統,有了目標知道該幹嘛。

沒有這些文檔,你是打算啃幾個星期代碼把系統是啥給啃出來???


未來的大趨勢是面向文檔編程,通過文檔生成代碼

只不過現在還沒有那麼通用化,只能用於是生成一些非常特化的代碼

但無數人都在為之努力


應該嚴格面向介面編程 並給介面編寫詳細文檔

通過函數簽名 (如果函數沒有副作用)

最多只能知道這個函數是輸入類型到返回類型的映射而已

很多函數的輸入類型的自然定義域並不是函數的定義域(輸入函數定義域以外的值就會產生錯誤或者UB)

函數定義域是這個自然定義域的一個子集

返回類型也只是一個陪域 而不是值域

這些如果不看文檔你是怎麼通過函數簽名得知的?

並且最重要的不看文檔你如何知道映射關係呢?(映射關係是通過文檔描述的 而不是實現代碼 實現代碼理論上是可替換的 只要滿足文檔描述的映射關係就行)

如果函數有副作用的話

那就更要看文檔了

不然你根本搞不清楚代碼正確的書寫次序

如果一個第三方庫沒有完整的文檔

我覺得就不可信賴(不管是否開源)

沒有文檔就等於介面行為沒有定義

介面行為沒有定義就可以隨意修改定義


看個毛線文檔,讓我寫文檔不差剝我一層皮。。。

我看到項目裡頭類似nm,tp的縮寫,罵娘的心都有了……

堅決定一個前端規範,誰給我寫這種鬼代碼我就砍死誰!

把所有與後端的交互都封起來,把這種不要命的縮寫都封在裡頭,Java資料庫你們隨便搞,別搞到我頭上就好了……

一般來說程序邏輯還是可以直接看代碼看懂的,不過真實的複雜業務邏輯還是夠嗆,這個還是需要做一些注釋說明的……

我就在模塊頂端開始寫更新日誌,一天天的寫,從功能到邏輯到業務,一點不漏的記下來


不是說不要文檔,也不是說不要通過文檔理解代碼。

什麼是文檔?代碼注釋也是文檔組成的一部分嘛。

只是說現在很多文檔都可以通過工具自動生成了,比如javadoc、rustdoc、powerdesign。但是一些個性化的文檔,還是可能需要手工整理完成的,特別是各種需求、設計分析文檔。

另外,文檔形式並不限於Word格式、網頁格式這樣的文件,很多團隊是把一些項目內容記錄在系統中的。

項目大了,光靠看代碼看注釋是行不通的。項目文檔更體現了一個項目的質量管理水平,沒注釋然後又是爛代碼,你咋看?


剛畢業半年,前幾個月和部門技術總監一起擼代碼。代碼規範、功能注釋、介面文檔一個都不落下,那個時候苦不連天,但個人技能提升是實打實在的。

現在換了一個部門,和一群意氣風發的小夥子做項目,沒規範、沒文檔,每天加班加點在改bug 滑稽?︵?凸

忘了說,我搞前端,老大搞後端。發現我代碼風格越來越向後端看齊(●"?"●)??


就你們這幫懶癌晚期的碼農,幾個迭代下來文檔還跟的上的有幾個?

除了代碼能相信啥? 連注釋我都不信


架構合理,命名規範 的代碼 確實不需要文檔 就可以很容易的看懂

但這肯定也只是 細節層次上的……

細節上的一些規範,高層次的架構,單一業務的狀態機,整個項目要做什麼事

這些不是簡單的 通過代碼 就可以非常明確的事情啊……

還有不和代碼直接相關的內容呢……

設計風格、顏色、形狀的限制,前端頁面的限制,尚未完成、暫時擱置的部分……


我司代碼幾個G,沒有文檔。


認同

我們已經不使用傳統那種噁心的文檔了

而是通過javadoc,在文檔中加入注釋,然後自動生成文檔

java9之後,javadoc都可以搜索了,很方便的

而且現在都講究self-explain code,意思是自我解釋的代碼

所以英語越來越重要起來

一般邏輯是這樣

先讀源代碼,方法名就能告訴你很多東西

其次看不懂,再讀注釋

注釋還看不懂,才去搜索

所以你要會用idea,要會用gradle,maven這些能夠自動下載注釋和源代碼的工具

否則找半天,這也是為什麼java比較重要的原因

因為java前兩步通過maven和gradle都自動化了


好的代碼連注釋都不用。

命名好的方法名,變數名,類名,介面名.……都是天然注釋。好的程序結構就是演算法說明。

什麼是爛代碼,就是一個方法寫了好幾百行,裡面一段一段的注釋,這就是沒系統訓練過編程。

好程序員的標準是,一行注釋都沒有,別人還能讀的很流暢。


很多人根本沒有理解為什麼寫文檔?

很多人尤其是中國的程序員根本無法理解什麼是團隊協作,這也是中國人做不了大項目。

寫文檔是為了節省自己和團隊的時間。 節省了時間才可以做更多的事情。

同樣的一個項目,你看文檔和看代碼的時間一樣嗎? 說白了如果把代碼作為一個產品,你看過哪個產品不需要說明和宣傳。 很多人不願意寫文檔認為浪費時間只不過是能力和素質較低造成的。


最好當然是代碼是能夠自己解釋自己,通過合理的結構,準確的命名,每個function即便沒有注釋都讓人一看就懂。 這是當代高級語言所要力求達到的效果。

二等的代碼是需要注釋的,通過短短一兩句的注釋,你能非常快的明白這段代碼的功能和需要注意的地方。

三等的代碼是需要文檔的,這其實是很差的,你代碼更新的時候未必會更新文檔,你看代碼的時候未必找得到文檔。

我覺得你領導說的雖然沒錯,但是如果你們代碼實現是很混亂不易讀的,又沒文檔注釋,團隊又新人很多,那還是安排人花點時間把文檔補齊吧。


絕對不過時,小項目還好,項目大了,沒有文檔,沒有介面定義,讓人如何接手?接手這種項目真是自求多福或者趕緊跑路,你看現在好點的開源項目哪個沒有文檔和介面規範的?

我現在做的一個項目就是幾乎沒有文檔,有很多與其它平台的api調用,這些api都沒有文檔記錄,代碼幾乎沒有注釋,名稱極其不規範,有些英文不明所以,還有很多拼寫錯誤,充滿魔術數字,各種0 6 9 4 1 魔術數字進行數值判斷,完全不知幹嘛,而且這個項目稍有差錯便影響整個公司的運作,維護這份代碼真想打人,每次修改都戰戰兢兢,代碼還是用ruby寫的,各種魔幻,html js亂成一鍋粥,js方法還存在被覆蓋的情況,簡直了,沒有設計模式,資料庫設計沒有文檔,幾乎沒有測試,維護這些項目無比痛苦,以前的代碼有 bug也很難改,一個改動需要全局搜索比對,現在5萬多行代碼了,需求還一直增加,實在不想搞了,求早日脫坑。


現在改一個同事寫的源碼,已經有一個星期了,沒文檔沒注釋沒流程說明,業務需求都得問甲方。更何況詭異的存儲過程觸發器配置文件,好幾本版本的IDE,好幾個版本的.net,耗費的時間已經可以重新寫一遍了。稍微一改全部報錯,況且軟體不能停止運行,最多5分鐘切換時間。感覺這個坑能把我埋了。

不想改代碼,只想逛逛知乎。。。


現在流行敏捷開發,至少要有個能跑的完整demo,有人承認項目的前景才開始補文檔的。

不過你這種情況,大佬想告訴你的是,趕緊去看代碼別在這BB,浪費時間。你再BB也不會有文檔冒出來啊。


別逗,項目小可以,項目大了還是靠文檔當index,否則你代碼要從哪裡入手呢。。。


推薦閱讀:

北大軟微VS北郵?
軟體工程專業前景怎麼樣?
如何評價太原理工大學軟體學院2016級起不再配發電腦?
中國的軟體業落後美國 50 年嗎?

TAG:程序員 | 軟體開發 | 軟體工程 | IT行業 |