亚洲熟妇av一区二区三区,久久久久久精品观看sss,免费观看四虎精品国产永久,国产成人精品一区二三区熟女,天堂网在线最新版www资源网

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

作者:cheaterlin,騰訊 PCG 后臺開發(fā)工程師

綜述

我寫過一篇《Code Review 我都 CR 些什么》,講解了 Code Review 對團隊有什么價值,我認(rèn)為 CR 最重要的原則有哪些。最近我在團隊工作中還發(fā)現(xiàn)了:

  • 原則不清晰。對于代碼架構(gòu)的原則,編碼的追求,我的骨干員工對它的認(rèn)識也不是很全面。當(dāng)前還是在 review 過程中我對他們口口相傳,總有遺漏。
  • 從知道到會做需要時間。我需要反復(fù)跟他們補充 review 他們漏掉的點,他們才能完成吸收、內(nèi)化,在后續(xù)的 review 過程中,能自己提出這些 review 的點。

過度文檔化是有害的,當(dāng)過多的內(nèi)容需要被閱讀,工程師們最終就會選擇不去讀,讀了也僅僅能吸收很少一部分。在 google,對于代碼細(xì)節(jié)的理解,更多還是口口相傳,在實踐中去感受和理解。但是,適當(dāng)?shù)奈臋n、文字宣傳,是必要的。特此,我就又輸出了這一篇文章,嘗試從'知名架構(gòu)原則'、'工程師的自我修養(yǎng)'、'不能上升到原則的幾個常見案例'三大模塊,把我個人的經(jīng)驗系統(tǒng)地輸出,供其他團隊參考。

知名架構(gòu)原則

后面原則主要受《程序員修煉之道: 通向務(wù)實的最高境界》、《架構(gòu)整潔之道》、《Unix 編程藝術(shù)》啟發(fā)。我不是第一個發(fā)明這些原則的人,甚至不是第一個總結(jié)出來的人,別人都已經(jīng)寫成書了!務(wù)實的程序員對于方法的總結(jié),總是殊途同歸。

細(xì)節(jié)即是架構(gòu)

(下面是原文摘錄, 我有類似觀點, 但是原文就寫得很好, 直接摘錄)

一直以來,設(shè)計(Design)和架構(gòu)(Architecture)這兩個概念讓大多數(shù)人十分迷惑–什么是設(shè)計?什么是架構(gòu)?二者究竟有什么區(qū)別?二者沒有區(qū)別。一丁點區(qū)別都沒有!"架構(gòu)"這個詞往往適用于"高層級"的討論中,這類討論一般都把"底層"的實現(xiàn)細(xì)節(jié)排除在外。而"設(shè)計"一詞,往往用來指代具體的系統(tǒng)底層組織結(jié)構(gòu)和實現(xiàn)的細(xì)節(jié)。但是,從一個真正的系統(tǒng)架構(gòu)師的日常工作來看,這些區(qū)分是根本不成立的。以給我設(shè)計新房子的建筑設(shè)計師要做的事情為例。新房子當(dāng)然是存在著既定架構(gòu)的,但這個架構(gòu)具體包含哪些內(nèi)容呢?首先,它應(yīng)該包括房屋的形狀、外觀設(shè)計、垂直高度、房間的布局,等等。

但是,如果查看建筑設(shè)計師使用的圖紙,會發(fā)現(xiàn)其中也充斥著大量的設(shè)計細(xì)節(jié)。譬如,我們可以看到每個插座、開關(guān)以及每個電燈具體的安裝位置,同時也可以看到某個開關(guān)與所控制的電燈的具體連接信息;我們也能看到壁爐的具體位置,熱水器的大小和位置信息,甚至是污水泵的位置;同時也可以看到關(guān)于墻體、屋頂和地基所有非常詳細(xì)的建造說明??偟膩碚f,架構(gòu)圖里實際上包含了所有的底層設(shè)計細(xì)節(jié),這些細(xì)節(jié)信息共同支撐了頂層的架構(gòu)設(shè)計,底層設(shè)計信息和頂層架構(gòu)設(shè)計共同組成了整個房屋的架構(gòu)文檔。

軟件設(shè)計也是如此。底層設(shè)計細(xì)節(jié)和高層架構(gòu)信息是不可分割的。他們組合在一起,共同定義了整個軟件系統(tǒng),缺一不可。所謂的底層和高層本身就是一系列決策組成的連續(xù)體,并沒有清晰的分界線。

我們編寫、review 細(xì)節(jié)代碼,就是在做架構(gòu)設(shè)計的一部分。我們編寫的細(xì)節(jié)代碼構(gòu)成了整個系統(tǒng)。我們就應(yīng)該在細(xì)節(jié) review 中,總是帶著所有架構(gòu)原則去審視。你會發(fā)現(xiàn),你已經(jīng)寫下了無數(shù)讓整體變得丑陋的細(xì)節(jié),它們背后,都有前人總結(jié)過的架構(gòu)原則。

把代碼和文檔綁在一起(自解釋原則)

寫文檔是個好習(xí)慣。但是寫一個別人需要咨詢老開發(fā)者才能找到的文檔,是個壞習(xí)慣。這個壞習(xí)慣甚至?xí)o工程師們帶來傷害。比如,當(dāng)初始開發(fā)者寫的文檔在一個犄角旮旯(在 wiki 里,但是閱讀代碼的時候沒有在明顯的位置看到鏈接),后續(xù)代碼被修改了,文檔已經(jīng)過時,有人再找出文檔來獲取到過時、錯誤的知識的時候,閱讀文檔這個同學(xué)的開發(fā)效率必然受到傷害。所以,如同 golang 的 godoc 工具能把代碼里'按規(guī)范來'的注釋自動生成一個文檔頁面一樣,我們應(yīng)該:

  • 按照 godoc 的要求好好寫代碼的注釋。
  • 代碼首先要自解釋,當(dāng)解釋不了的時候,需要就近、合理地寫注釋。
  • 當(dāng)小段的注釋不能解釋清楚的時候,應(yīng)該有 doc.go 來解釋,或者,在同級目錄的 ReadMe.md 里注釋講解。
  • 文檔需要強大的富文本編輯能力,Down 無法滿足,可以寫到 wiki 里,同時必須把 wiki 的簡單描述和鏈接放在代碼里合適的位置。讓閱讀和維護代碼的同學(xué)一眼就看到,能做到及時的維護。

以上,總結(jié)起來就是,解釋信息必須離被解釋的東西,越近越好。代碼能做到自解釋,是最棒的。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

讓目錄結(jié)構(gòu)自解釋

ETC 價值觀(easy to change)

ETC 是一種價值觀念,不是一條原則。價值觀念是幫助你做決定的: 我應(yīng)該做這個,還是做那個?當(dāng)你在軟件領(lǐng)域思考時,ETC 是個向?qū)?,它能幫助你在不同的路線中選出一條。就像其他一些價值觀念一樣,你應(yīng)該讓它漂浮在意識思維之下,讓它微妙地將你推向正確的方向。

敏捷軟件工程,所謂敏捷,就是要能快速變更,并且在變更中保持代碼的質(zhì)量。所以,持有 ETC 價值觀看待代碼細(xì)節(jié)、技術(shù)方案,我們將能更好地編寫出適合敏捷項目的代碼。這是一個大的價值觀,不是一個基礎(chǔ)微觀的原則,所以沒有例子。本文提到的所有原則,或者接,或間接,都要為 ETC 服務(wù)。

DRY 原則(don not repeat yourself)

在《Code Review 我都 CR 些什么》里面,我已經(jīng)就 DRY 原則做了深入闡述,這里不再贅述。我認(rèn)為 DRY 原則是編碼原則中最重要的編碼原則,沒有之一(ETC 是個觀念)。不要重復(fù)!不要重復(fù)!不要重復(fù)!

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

正交性原則(全局變量的危害)

'正交性'是幾何學(xué)中的術(shù)語。我們的代碼應(yīng)該消除不相關(guān)事物之間的影響。這是一給簡單的道理。我們寫代碼要'高內(nèi)聚、低耦合',這是大家都在提的。

但是,你有為了使用某個 class 一堆能力中的某個能力而去派生它么?你有寫過一個 helper 工具,它什么都做么?在騰訊,我相信你是做過的。你自己說,你這是不是為了復(fù)用一點點代碼,而讓兩大塊甚至多塊代碼耦合在一起,不再正交了?大家可能并不是不明白正交性的價值,只是不知道怎么去正交。手段有很多,但是首先我就要批判一下 OOP。它的核心是多態(tài),多態(tài)需要通過派生/繼承來實現(xiàn)。繼承樹一旦寫出來,就變得很難 change,你不得不為了使用一小段代碼而去做繼承,讓代碼耦合。

你應(yīng)該多使用組合,而不是繼承。以及,應(yīng)該多使用 DIP(Dependence Inversion Principle),依賴倒置原則。換個說法,就是面向 interface 編程,面向契約編程,面向切面編程,他們都是 DIP 的一種衍生。寫 golang 的同學(xué)就更不陌生了,我們要把一個 struct 作為一個 interface 來使用,不需要顯式 implement/extend,僅僅需要持有對應(yīng) interface 定義了的函數(shù)。這種 duck interface 的做法,讓 DIP 來得更簡單。AB 兩個模塊可以獨立編碼,他們僅僅需要一個依賴一個 interface 簽名,一個剛好實現(xiàn)該 interface 簽名。并不需要顯式知道對方 interface 簽名的兩個模塊就可以在需要的模塊、場景下被組合起來使用。代碼在需要被組合使用的時候才產(chǎn)生了一點關(guān)系,同時,它們依然保持著獨立。

說個正交性的典型案例。全局變量是不正交的!沒有充分的理由,禁止使用全局變量。全局變量讓依賴了該全局變量的代碼段互相耦合,不再正交。特別是一個 pkg 提供一個全局變量給其他模塊修改,這個做法會讓 pkg 之間的耦合變得復(fù)雜、隱秘、難以定位。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

全局 map case

單例就是全局變量

這個不需要我解釋,大家自己品一品。后面有'共享狀態(tài)就是不正確的狀態(tài)'原則,會進(jìn)一步講到。我先給出解決方案,可以通過管道、消息機制來替代共享狀態(tài)/使用全局變量/使用單例。僅僅能獲取此刻最新的狀態(tài),通過消息變更狀態(tài)。要拿到最新的狀態(tài),需要重新獲取。在必要的時候,引入鎖機制。

可逆性原則

可逆性原則是很少被提及的一個原則??赡嫘?,就是你做出的判斷,最好都是可以被逆轉(zhuǎn)的。再換一個容易懂的說法,你最好盡量少認(rèn)為什么東西是一定的、不變的。比如,你認(rèn)為你的系統(tǒng)永遠(yuǎn)服務(wù)于,用 32 位無符號整數(shù)(比如 QQ 號)作為用戶標(biāo)識的系統(tǒng)。你認(rèn)為,你的持久化存儲,就選型 SQL 存儲了。當(dāng)這些一開始你認(rèn)為一定的東西,被推翻的時候,你的代碼卻很難去 change,那么,你的代碼就是可逆性做得很差。書里有一個例證,我覺得很好,直接引用過來。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

與其認(rèn)為決定是被刻在石頭上的,還不如把它們想像成寫在沙灘的沙子上。一個大浪隨時都可能襲來,卷走一切。騰訊也確實在 20 年內(nèi)經(jīng)歷了'大鐵塊'到'云虛擬機換成容器'的幾個階段。幾次變化都是傷筋動骨,浪費大量的時間。甚至總會有一些上一個時代殘留的服務(wù)。就機器數(shù)量而論,還不小。一到裁撤季,就很難受。就最近,我看到某個 trpc 插件,直接從環(huán)境變量里讀取本機 IP,僅僅因為 STKE(Tencent Kubernetes Engine)提供了這個能力。這個細(xì)節(jié)設(shè)計就是不可逆的,將來會有人為它買單,可能價格還不便宜。

我今天才想起一個事兒。當(dāng)年 SNG 的很多部門對于 metrics 監(jiān)控的使用。就潛意識地認(rèn)為,我們將一直使用'模塊間調(diào)用監(jiān)控'組件。使用它的 API 是直接把上報通道 DCLog 的 API 裸露在業(yè)務(wù)代碼里的。今天(2020.12.01),該組件應(yīng)該已經(jīng)完全沒有人維護、完全下線了,這些核心業(yè)務(wù)代碼要怎么辦?有人能對它做出修改么?那,這些部門現(xiàn)在還有 metrics 監(jiān)控么?答案,可能是悲觀的。有人已經(jīng)已經(jīng)嘗到了可逆性之痛。

依賴倒置原則(DIP)

DIP 原則太重要了,我這里單獨列一節(jié)來講解。我這里只是簡單的講解,講解它最原始和簡單的形態(tài)。依賴倒置原則,全稱是 Dependence Inversion Principle,簡稱 DIP。考慮下面這幾段代碼:

package dippackage diptype Botton interface {    TurnOn()    TurnOff()}type UI struct {    botton Botton}func NewUI(b Botton) *UI {    return &UI{botton: b}}func (u *UI) Poll() {    u.botton.TurnOn()    u.botton.TurnOff()    u.botton.TurnOn()}

package Javaimplimport "fmt"type Lamp struct {}func NewLamp() *Lamp {    return &Lamp{}}func (*Lamp) TurnOn() {    fmt.Println("turn on java lamp")}func (*Lamp) TurnOff() {    fmt.Println("turn off java lamp")}

package pythonimplimport "fmt"type Lamp struct {}func NewLamp() *Lamp {    return &Lamp{}}func (*Lamp) TurnOn() {    fmt.Println("turn on python lamp")}func (*Lamp) TurnOff() {    fmt.Println("turn off python lamp")}

package mainimport (    "javaimpl"    "pythonimpl"    "dip")func runPoll(b dip.Botton) {    ui := NewUI(b)    ui.Poll()}func main() {    runPoll(pythonimpl.NewLamp())    runPoll(javaimpl.NewLamp())}

看代碼,main pkg 里的 runPoll 函數(shù)僅僅面向 Botton interface 編碼,main pkg 不再關(guān)心 Botton interface 里定義的 TurnOn、TurnOff 的實現(xiàn)細(xì)節(jié)。實現(xiàn)了解耦。這里,我們能看到 struct UI 需要被注入(inject)一個 Botton interface 才能邏輯完整。所以,DIP 經(jīng)常換一個名字出現(xiàn),叫做依賴注入(Dependency Injection)。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

從這個依賴圖觀察。我們發(fā)現(xiàn),一般來說,UI struct 的實現(xiàn)是要應(yīng)該依賴于具體的 pythonLamp、javaLamp、其他各種 Lamp,才能讓自己的邏輯完整。那就是 UI struct 依賴于各種 Lamp 的實現(xiàn),才能邏輯完整。但是,我們看上面的代碼,卻是反過來了。pythonLamp、javaLamp、其他各種 Lamp 是依賴 Botton interface 的定義,才能用來和 UI struct 組合起來拼接成完整的業(yè)務(wù)邏輯。變成了,Lamp 的實現(xiàn)細(xì)節(jié),依賴于 UI struct 對于 Botton interface 的定義。這個時候,你發(fā)現(xiàn),這種依賴關(guān)系被倒置了!依賴倒置原則里的'倒置',就是這么來的。在 golang 里,'pythonLamp、javaLamp、其他各種 Lamp 是依賴 Botton interface 的定義',這個依賴是隱性的,沒有顯式的 implement 和 extend 關(guān)鍵字。代碼層面,pkg dip 和 pkg pythonimpl、javaimpl 沒有任何依賴關(guān)系。他們僅僅需要被你在 main pkg 里組合起來使用。

在 J2EE 里,用戶的業(yè)務(wù)邏輯不再依賴低具體低層的各種存儲細(xì)節(jié),而僅僅依賴一套配置化的 Java Bean 接口。Object 落地存儲的具體細(xì)節(jié),被做成了 Java Bean 配置,注入到框架里。這就是 J2EE 的核心科技,并不復(fù)雜,其實也沒有多么'高不可攀'。反而,在'動態(tài)代碼'優(yōu)于'配置'的今天,這種通過配置實現(xiàn)的依賴注入,反而有點過時了。

將知識用純文本來保存

這也是一個生僻的原則。指代碼操作的數(shù)據(jù)和方案設(shè)計文稿,如果沒有充分的必要使用特定的方案,就應(yīng)該使用人類可讀的文本來保存、交互。對于方案設(shè)計文稿,你能不使用 office 格式,就不使用(office 能極大提升效率,才用),最好是原始 text。這是《Unix 編程藝術(shù)》也提到了的 Unix 系產(chǎn)生的設(shè)計信條。簡而言之一句話,當(dāng)需要確保有一個所有各方都能使用的公共標(biāo)準(zhǔn),才能實現(xiàn)交互溝通時,純文本就是這個標(biāo)準(zhǔn)。它是一個接受度最高的通行標(biāo)準(zhǔn)。如果沒有必要的理由,我們就應(yīng)該使用純文本。

契約式設(shè)計

如果你對契約式設(shè)計(Design by Contract, DBC)還很陌生,我相信,你和其他端的同學(xué)(web、client、后端)聯(lián)調(diào)需求應(yīng)該是一件很花費時間的事情。你自己編寫接口自動化,也會是一件很耗費精力的事情。你先看看它的wiki 解釋吧。grpc grpc-gateway swagger 是個很香的東西。

代碼是否不多不少剛好完成它宣稱要做的事情,可以使用契約加以校驗和文檔化。TDD 就是全程在不斷調(diào)整和履行著契約。TDD(Test-Driven Development)是自底向上地編碼過程,其實會耗費大量的精力,并且對于一個良好的層級架構(gòu)沒有幫助。TDD 不是強推的規(guī)范,但是同學(xué)們可以用一用,感受一下。TDD 方法論實現(xiàn)的接口、函數(shù),自我解釋能力一般來說比較強,因為它就是一個實現(xiàn)契約的過程。

拋開 TDD 不談。我們的函數(shù)、api,你能快速抓住它描述的核心契約么?它的契約簡單么?如果不能、不簡單,那你應(yīng)該要求被 review 的代碼做出調(diào)整。如果你在指導(dǎo)一個后輩,你應(yīng)該幫他思考一下,給出至少一個可能的簡化、拆解方向。

盡早崩潰

Erlang 和 Elixir 語言信奉這種哲學(xué)。喬-阿姆斯特朗,Erlang 的發(fā)明者,《Erlang 程序設(shè)計》的作者,有一句反復(fù)被引用的話: "防御式編程是在浪費時間,讓它崩潰"。

盡早崩潰不是說不容錯,而是程序應(yīng)該被設(shè)計成允許出故障,有適當(dāng)?shù)墓收媳O(jiān)管程序和代碼,及時告警,告知工程師,哪里出問題了,而不是嘗試掩蓋問題,不讓程序員知道。當(dāng)最后程序員知道程序出故障的時候,已經(jīng)找不到問題出現(xiàn)在哪里了。

特別是一些 recover 之后什么都不做的代碼,這種代碼簡直是毒瘤!當(dāng)然,崩潰,可以是早一些向上傳遞 error,不一定就是 panic。同時,我要求大家不要在沒有充分的必要性的時候 panic,應(yīng)該更多地使用向上傳遞 error,做好 metrics 監(jiān)控。合格的 golang 程序員,都不會在沒有必要的時候無視 error,會妥善地做好 error 處理、向上傳遞、監(jiān)控。一個死掉的程序,通常比一個癱瘓的程序,造成的損害要小得多。

崩潰但是不告警,或者沒有補救的辦法,不可取.盡早崩潰的題外話是,要在問題出現(xiàn)的時候做合理的告警,有預(yù)案,不能掩蓋,不能沒有預(yù)案:

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

解耦代碼讓改變?nèi)菀?/strong>

這個原則,顯而易見,大家自己也常常提,其他原則或多或少都和它有關(guān)系。但是我也再提一提。我主要是描述一下它的癥狀,讓同學(xué)們更好地警示自己'我這兩塊代碼是不是耦合太重,需要額外引入解耦的設(shè)計了'。癥狀如下:

  • 不相關(guān)的 pkg 之間古怪的依賴關(guān)系
  • 對一個模塊進(jìn)行的'簡單'修改,會傳播到系統(tǒng)中不相關(guān)的模塊里,或是破壞了系統(tǒng)中的其他部分
  • 開發(fā)人員害怕修改代碼,因為他們不確定會造成什么影響
  • 會議要求每個人都必須參加,因為沒有人能確定誰會受到變化的影響

只管命令不要詢問

看看如下三段代碼:

func applyDiscount(customer Customer, orderID string, discount float32) { customer.  Orders.  Find(orderID).  GetTotals().  ApplyDiscount(discount)}

func applyDiscount(customer Customer, orderID string, discount float32) { customer.  FindOrder(orderID).  GetTotals().  ApplyDiscount(discount)}

func applyDiscount(customer Customer, orderID string, discount float32) { customer.  FindOrder(orderID).  ApplyDiscount(discount)}

明顯,最后一段代碼最簡潔。不關(guān)心 Orders 成員、總價的存在,直接命令 customer 找到 Order 并對其進(jìn)行打折。當(dāng)我們調(diào)整 Orders 成員、GetTotals()方法的時候,這段代碼不用修改。還有一種更嚇人的寫法:

func applyDiscount(customer Customer, orderID string, discount float32) { total := customer.  FindOrder(orderID).  GetTotals() customer.  FindOrder(orderID).  SetTotal(total*discount)}

它做了更多的查詢,關(guān)心了更多的細(xì)節(jié),變得更加 hard to change 了。我相信,大家寫過類似的代碼也不少。特別是客戶端同學(xué)。

最好的那一段代碼,就是只管給每個 struct 發(fā)送命令,要求大家做事兒。怎么做,就內(nèi)聚在和 struct 關(guān)聯(lián)的方法里,其他人不要去操心。一旦其他人操心了,當(dāng)需要做修改的時候,就要操心了這個細(xì)節(jié)的人都一起參與進(jìn)修改過程。

不要鏈?zhǔn)秸{(diào)用方法

看下面的例子:

func amount(customer Customer) float32 { return customer.Orders.Last().Totals().Amount}

func amount(totals Totals) float32 { return totals.Amount}

第二個例子明顯優(yōu)于第一個,它變得更簡單、通用、ETC。我們應(yīng)該給函數(shù)傳入它關(guān)心的最小集合作為參數(shù)。而不是,我有一個 struct,當(dāng)某個函數(shù)需要這個 struct 的成員的時候,我們把整個 struct 都作為參數(shù)傳遞進(jìn)去。應(yīng)該僅僅傳遞函數(shù)關(guān)心的最小集合。傳進(jìn)去的一整條調(diào)用鏈對函數(shù)來說,都是無關(guān)的耦合,只會讓代碼更 hard to change,讓工程師懼怕去修改。這一條原則,和上一條關(guān)系很緊密,問題常常同時出現(xiàn)。還是,特別是在客戶端代碼里。

繼承稅(多用組合)

繼承就是耦合。不僅子類耦合到父類,以及父類的父類等,而且使用子類的代碼也耦合到所有祖先類。 有些人認(rèn)為繼承是定義新類型的一種方式。他們喜歡設(shè)計圖表,會展示出類的層次結(jié)構(gòu)。他們看待問題的方式,與維多利亞時代的紳士科學(xué)家們看待自然的方式是一樣的,即將自然視為須分解到不同類別的綜合體。 不幸的是,這些圖表很快就會為了表示類之間的細(xì)微差別而逐層添加,最終可怕地爬滿墻壁。由此增加的復(fù)雜性,可能使應(yīng)用程序更加脆弱,因為變更可能在許多層次之間上下波動。 因為一些值得商榷的詞義消歧方面的原因,C 在20世紀(jì)90年代玷污了多重繼承的名聲。結(jié)果,許多當(dāng)下的OO語言都沒有提供這種功能。

因此,即使你很喜歡復(fù)雜的類型樹,也完全無法為你的領(lǐng)域準(zhǔn)確地建模。

Java 下一切都是類。C 里不使用類還不如使用 C。寫 Python、PHP,我們也肯定要時髦地寫一些類。寫類可以,當(dāng)你要去繼承,你就得考慮清楚了。繼承樹一旦形成,就是非常 hard to change 的,在敏捷項目里,你要想清楚'代價是什么',有必要么?這個設(shè)計'可逆'么?對于邊界清晰的 UI 框架、游戲引擎,使用復(fù)雜的繼承樹,挺好的。對于 UI 邏輯、后臺邏輯,可能,你僅僅需要組合、DIP(依賴反轉(zhuǎn))技術(shù)、契約式編程(接口與協(xié)議)就夠了。寫出繼承樹不是'就應(yīng)該這么做',它是成本,繼承是要收稅的!

在 golang 下,繼承稅的煩惱被減輕了,golang 從來說自己不是 OO 的語言,但是你 OO 的事情,我都能輕松地做到。更進(jìn)一步,OO 和過程式編程的區(qū)別到底是什么?

面向過程,面向?qū)ο?,函?shù)式編程。三種編程結(jié)構(gòu)的核心區(qū)別,是在不同的方向限制程序員,來做到好的代碼結(jié)構(gòu)(引自《架構(gòu)整潔之道》):

  • 結(jié)構(gòu)化編程是對程序控制權(quán)的直接轉(zhuǎn)移的限制。
  • 面向?qū)ο笫菍Τ绦蚩刂茩?quán)的間接轉(zhuǎn)移的限制。
  • 函數(shù)式編程是對程序中賦值操作的限制。

SOLID 原則(單一功能、開閉原則、里氏替換、接口隔離、依賴反轉(zhuǎn),后面會講到)是 OOP 編程的最經(jīng)典的原則。其中 D 是指依賴倒置原則(Dependence Inversion Principle),我認(rèn)為,是 SOLID 里最重要的原則。J2EE 的 container 就是圍繞 DIP 原則設(shè)計的。DIP 能用于避免構(gòu)建復(fù)雜的繼承樹,DIP 就是'限制控制權(quán)的間接轉(zhuǎn)移'能繼續(xù)發(fā)揮積極作用的最大保障。合理使用 DIP 的 OOP 代碼才可能是高質(zhì)量的代碼。

golang 的 interface 是 duck interface,把 DIP 原則更進(jìn)一步,不需要顯式 implement/extend interface,就能做到 DIP。golang 使用結(jié)構(gòu)化編程范式,卻有面向?qū)ο缶幊谭妒降暮诵膬?yōu)點,甚至簡化了。這是一個基于高度抽象理解的極度精巧的設(shè)計。google 把 abstraction 這個設(shè)計理念發(fā)揮到了極致。曾經(jīng),J2EE 的 container(EJB, Java Bean)設(shè)計是國內(nèi) Java 程序員引以為傲'架構(gòu)設(shè)計'、'厲害的設(shè)計'。

在 golang 里,它被分析、解構(gòu),以更簡單、靈活、統(tǒng)一、易懂的方式呈現(xiàn)出來。寫了多年垃圾 C 代碼的騰訊后端工程師們,是你們再次審視 OOP 的時候了。我大學(xué)一年級的時候看的 C 教材,終歸給我描述了一個美好卻無法抵達(dá)的世界。目標(biāo)我沒有放棄,但我不再用 OOP,而是更多地使用組合(Mixin)。寫 golang 的同學(xué),應(yīng)該對 DIP 和組合都不陌生,這里我不再贅述。如果有人自傲地說他在 golang 下搞起了繼承,我只能說,'同志,你現(xiàn)在站在了廣大 gopher 的對立面'?,F(xiàn)在,你站在哲學(xué)的云端,鳥瞰了 Structured Programming 和 OOP。你還愿意再繼續(xù)支付繼承稅么?

共享狀態(tài)是不正確的狀態(tài)

你坐在最喜歡的餐廳。吃完主菜,問男服務(wù)員還有沒有蘋果派。他回頭一看-陳列柜里還有一個,就告訴你"還有"。點到了蘋果派,你心滿意足地長出了一口氣。與此同時,在餐廳的另一邊,還有一個顧客也問了女服務(wù)員同樣的問題。她也看了看,確認(rèn)有一個,讓顧客點了單。總有一個顧客會失望的。

問題出在共享狀態(tài)。餐廳里的每一個服務(wù)員都查看了陳列柜,卻沒有考慮到其他服務(wù)員。你們可以通過加互斥鎖來解決正確性的問題,但是,兩個顧客有一個會失望或者很久都得不到答案,這是肯定的。

所謂共享狀態(tài),換個說法,就是: 由多個人查看和修改狀態(tài)。這么一說,更好的解決方案就浮出水面了: 將狀態(tài)改為集中控制。預(yù)定蘋果派,不再是先查詢,再下單。而是有一個餐廳經(jīng)理負(fù)責(zé)和服務(wù)員溝通,服務(wù)員只管發(fā)送下單的命令/消息,經(jīng)理看情況能不能滿足服務(wù)員的命令。

這種解決方案,換一個說法,也可以說成"用角色實現(xiàn)并發(fā)性時不必共享狀態(tài)"。對,上面,我們引入了餐廳經(jīng)理這個角色,賦予了他職責(zé)。當(dāng)然,我們僅僅應(yīng)該給這個角色發(fā)送命令,不應(yīng)該去詢問他。前面講過了,'只管命令不要詢問',你還記得么。

同時,這個原則就是 golang 里大家耳熟能詳?shù)闹V語: "不要通過共享內(nèi)存來通信,而應(yīng)該通過通信來共享內(nèi)存"。作為并發(fā)性問題的根源,內(nèi)存的共享備受關(guān)注。但實際上,在應(yīng)用程序代碼共享可變資源(文件、數(shù)據(jù)庫、外部服務(wù))的任何地方,問題都有可能冒出來。當(dāng)代碼的兩個或多個實例可以同時訪問某些資源時,就會出現(xiàn)潛在的問題。

緘默原則

如果一個程序沒什么好說,就保持沉默。過多的正常日志,會掩蓋錯誤信息。過多的信息,會讓人根本不再關(guān)注新出現(xiàn)的信息,'更多信息'變成了'沒有信息'。每人添加一點信息,就變成了輸出很多信息,最后等于沒有任何信息。

  • 不要在正常 case 下打印日志。
  • 不要在單元測試?yán)锸褂?fmt 標(biāo)準(zhǔn)輸出,至少不要提交到 master。
  • 不打不必要的日志。當(dāng)錯誤出現(xiàn)的時候,會非常明顯,我們能第一時間反應(yīng)過來并處理。
  • 讓調(diào)試的日志停留在調(diào)試階段,或者使用較低的日志級別,你的調(diào)試信息,對其他人根本沒有價值。
  • 即使低級別日志,也不能泛濫。不然,日志打開與否都沒有差別,日志變得毫無價值。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

緘默

錯誤傳遞原則

我不喜歡 Java 和 C 的 exception 特性,它容易被濫用,它具有傳染性(如果代碼 throw 了 excepttion, 你就得 handle 它,不 handle 它,你就崩潰了??赡苣悴幌M罎?,你僅僅希望報警)。但是 exception(在 golang 下是 panic)是有價值的,參考微軟的文章:

Exceptions are preferred in modern C  for the following reasons:* An exception forces calling code to recognize an error condition and handle it. Unhandled exceptions stop program execution.* An exception jumps to the point in the call stack that can handle the error. Intermediate functions can let the exception propagate. They don't have to coordinate with other layers.* The exception stack-unwinding mechanism destroys all objects in scope after an exception is thrown, according to well-defined rules.* An exception enables a clean separation between the code that detects the error and the code that handles the error.

Google 的 C 規(guī)范在常規(guī)情況禁用 exception,理由包含如下內(nèi)容:

Because most existing C  code at Google is not prepared to deal with exceptions, it is comparatively difficult to adopt new code that generates exceptions.

從 google 和微軟的文章中,我們不難總結(jié)出以下幾點衍生的結(jié)論:

  • 在必要的時候拋出 exception。使用者必須具備'必要性'的判斷能力。
  • exception 能一路把底層的異常往上傳遞到高函數(shù)層級,信息被向上傳遞,并且在上級被妥善處理??梢宰尞惓:完P(guān)心具體異常的處理函數(shù)在高層級和低層級遙相呼應(yīng),中間層級什么都不需要做,僅僅向上傳遞。
  • exception 傳染性很強。當(dāng)代碼由多人協(xié)作,使用 A 模塊的代碼都必須要了解它可能拋出的異常,做出合理的處理。不然,就都寫一個丑陋的 catch,catch 所有異常,然后做一個沒有針對性的處理。每次 catch 都需要加深一個代碼層級,代碼常常寫得很丑。

我們看到了異常的優(yōu)缺點。上面第二點提到的信息傳遞,是很有價值的一點。golang 在 1.13 版本中拓展了標(biāo)準(zhǔn)庫,支持了Error Wrapping也是承認(rèn)了 error 傳遞的價值。

所以,我們認(rèn)為錯誤處理,應(yīng)該具備跨層級的錯誤信息傳遞能力,中間層級如果不關(guān)心,就把 error 加上本層的信息向上透傳(有時候可以直接透傳),應(yīng)該使用 Error Wrapping。exception/panic 具有傳染性。大量使用,會讓代碼變得丑陋,同時容易滋生可讀性問題。我們應(yīng)該多使用 Error Wrapping,在必要的時候,才使用 exception/panic。每一次使用 exception/panic,都應(yīng)該被認(rèn)真審核。需要 panic 的地方,不去 panic,也是有問題的。參考本文的'盡早崩潰'。

額外說一點,注意不要把整個鏈路的錯誤信息帶到公司外,帶到用戶的瀏覽器、native 客戶端。至少不能直接展示給用戶看到。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

錯誤鏈

SOLID

SOLID 原則,是由以下幾個原則的集合體:

  • SRP: 單一職責(zé)原則
  • OCP: 開閉原則
  • LSP: 里氏替換原則
  • ISP: 接口隔離原則
  • DIP: 依賴反轉(zhuǎn)原則

這些年來,這幾個設(shè)計原則在很多不同的出版物里都有過詳細(xì)描述。它們太出名了,我這里就不更多地做詳解了。我這里想說的是,這 5 個原則環(huán)環(huán)相扣,前 4 個原則,要么就是同時做到,要么就是都沒做到,很少有說,做到其中一點其他三點都不滿足。ISP 就是做到 LSP 的常用手段。ISP 也是做到 DIP 的基礎(chǔ)。只是,它剛被提出來的時候,是主要針對'設(shè)計繼承樹'這個目的的?,F(xiàn)在,它們已經(jīng)被更廣泛地使用在模塊、領(lǐng)域、組件這種更大的概念上。

SOLI 都顯而易見,DIP 原則是最值得注意的一點,我在其他原則里也多次提到了它。如果你還不清楚什么是 DIP,一定去看明白。這是工程師最基礎(chǔ)、必備的知識點之一了。

要做到 OCP 開閉原則,其實,就是要大家要通過后面講到的'不要面向需求編程'才能做好。如果你還是面向需求、面向 UI、交互編程,你永遠(yuǎn)做不到開閉,并且不知道如何才能做到開閉。

如果你對這些原則確實不了解,建議讀一讀《架構(gòu)整潔之道》。該書的作者 Bob 大叔,就是第一個提出 SOLID 這個集合體的人(20 世紀(jì) 80 年代末,在 USENET 新聞組)。

一個函數(shù)不要出現(xiàn)多個層級的代碼

// IrisFriends 拉取好友func IrisFriends(ctx iris.Context, app *app.App) { var rsp sdc.FriendsRsp defer func() {  var buf bytes.Buffer  _ = (&jsonpb.Marshaler{EmitDefaults: true}).Marshal(&buf, &rsp)  _, _ = ctx.Write(buf.Bytes()) }() common.AdjustCookie(ctx) if !checkCookie(ctx) {  return } // 從cookie中拿到關(guān)鍵的登陸態(tài)等有效信息 var session common.BaseSession common.GetBaseSessionFromCookie(ctx, &session) // 校驗登陸態(tài) err := common.CheckLoginSig(session, app.ConfigStore.Get().OIDBCmdSetting.PTLogin) if err != nil {  _ = common.ErrorResponse(ctx, errors.PTSigErr, 0, "check login sig error")  return } if err = getRelationship(ctx, app.ConfigStore.Get().OIDBCmdSetting, NewAPI(), &rsp); err != nil {  // TODO:日志 } return}

上面這一段代碼,是我隨意找的一段代碼。邏輯非常清晰,因為除了最上面 defer 寫回包的代碼,其他部分都是頂層函數(shù)組合出來的。閱讀代碼,我們不會掉到細(xì)節(jié)里出不來,反而忽略了整個業(yè)務(wù)流程。同時,我們能明顯發(fā)現(xiàn)它沒寫完,以及 common.ErrorResponse 和 defer func 兩個地方都寫了回包,可能出現(xiàn)發(fā)起兩次 http 回包。TODO 也會非常顯眼。

想象一下,我們沒有把細(xì)節(jié)收歸進(jìn) checkCookie()、getRelationship()等函數(shù),而是展開在這里,但是總函數(shù)行數(shù)沒有到 80 行,表面上符合規(guī)范。但是實際上,閱讀代碼的同學(xué)不再能輕松掌握業(yè)務(wù)邏輯,而是同時在閱讀功能細(xì)節(jié)和業(yè)務(wù)流程。閱讀代碼變成了每個時刻心智負(fù)擔(dān)都很重的事情。

顯而易見,單個函數(shù)里應(yīng)該只保留某一個層級(layer)的代碼,更細(xì)化的細(xì)節(jié)應(yīng)該被抽象到下一個 layer 去,成為子函數(shù)。

Unix 哲學(xué)基礎(chǔ)

《Code Review 我都 CR 些什么》講解了很多 Unix 的設(shè)計哲學(xué)。這里不再贅述,僅僅列舉一下。大家自行閱讀和參悟,并且運用到編碼、review 活動中。

  • 模塊原則: 使用簡潔的接口拼合簡單的部件
  • 清晰原則: 清晰勝于技巧
  • 組合原則: 設(shè)計時考慮拼接組合
  • 分離原則: 策略同機制分離,接口同引擎分離
  • 簡潔原則: 設(shè)計要簡潔,復(fù)雜度能低則低
  • 吝嗇原則: 除非確無它法,不要編寫龐大的程序
  • 透明性原則: 設(shè)計要可見,以便審查和調(diào)試
  • 健壯原則: 健壯源于透明與簡潔
  • 表示原則: 把知識疊入數(shù)據(jù)以求邏輯質(zhì)樸而健壯
  • 通俗原則: 接口設(shè)計避免標(biāo)新立異
  • 緘默原則: 如果一個程序沒什么好說,就保持沉默
  • 補救原則: 出現(xiàn)異常時,馬上退出并給出足量錯誤信息
  • 經(jīng)濟原則: 寧花機器一分,不花程序員一秒
  • 生成原則: 避免手工 hack,盡量編寫程序去生成程序
  • 優(yōu)化原則: 雕琢前先得有原型,跑之前先學(xué)會走
  • 多樣原則: 絕不相信所謂"不二法門"的斷言
  • 擴展原則: 設(shè)計著眼未來,未來總比預(yù)想快

工程師的自我修養(yǎng)

下面,是一些在 review 細(xì)節(jié)中不能直接使用的原則。更像是一種信念和自我約束。帶著這些信念去編寫、review 代碼,把這些信念在實踐中傳遞下去,將是極有價值的。

偏執(zhí)

對代碼細(xì)節(jié)偏執(zhí)的觀念,是我自己提出的新觀點。在當(dāng)下研發(fā)質(zhì)量不高的騰訊,是很有必要普遍存在的一個觀念。在一個系統(tǒng)不完善、時間安排荒謬、工具可笑、需求不可能實現(xiàn)的世界里,讓我們安全行事吧。就像伍迪-艾倫說的:"當(dāng)所有人都真的在給你找麻煩的時候,偏執(zhí)就是一個好主意。"

對于一個方案,一個實現(xiàn),請不要說出"好像這樣也可以"。你一定要選出一個更好的做法,并且一直堅持這個做法,并且要求別人也這樣做。既然他來讓你 review 了,你就要有自己的偏執(zhí),你一定要他按照你覺得合適的方式去做。當(dāng)然,你得有說服得了自己,也說服得了他人的理由。即使,只有一點點。偏執(zhí)會讓你的世界變得簡單,你的團隊的協(xié)作變得簡單。特別當(dāng)你身處一個編碼質(zhì)量低下的團隊的時候。你至少能說,我是一個務(wù)實的程序員。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

控制軟件的熵是軟件工程的重要任務(wù)之一

熵是個物理學(xué)概念,大家可能看過諾蘭的電影《信條》。簡單來說,熵可以理解為'混亂程度'。我們的項目,在剛開始的幾千行代碼,是很簡潔的。但是,為什么到了 100w 行,我們常常就感覺'太復(fù)雜了'?比如 QQ 客戶端,最近終于在做大面積重構(gòu),但是發(fā)現(xiàn)無數(shù) crash。其中一個重要原因,就是'混亂程度'太高了。'混亂程度',理解起來還是比較抽象,它有很多其他名字。'hard code 很多'、'特殊邏輯很多'、'定制化邏輯很多'。再換另一個抽象的說法,'我們面對一類問題,采取了過多的范式和特殊邏輯細(xì)節(jié)去實現(xiàn)它'。

熵,是一點點堆疊起來的,在一個需求的 2000 行代碼更改中,你可能就引入了一個不同的范式,打破了之前的通用范式。在微觀來看,你覺得你的代碼是'整潔干凈'的。就像一個已經(jīng)穿著好看的紅色風(fēng)衣的人,你隔一天讓他接著穿上一條綠色的褲子,這還干凈整潔么?熵,在不斷增加,我們需要做到以下幾點,不然你的團隊將在希望通過重構(gòu)來降低項目的熵的時候嘗到惡果,甚至放棄重構(gòu),讓熵不斷增長下去。

  • 如果沒有充分的理由,始終使用項目規(guī)范的范式對每一類問題做出解決方案。
  • 如果業(yè)務(wù)發(fā)展發(fā)現(xiàn)老的解決方案不再優(yōu)秀,做整體重構(gòu)。
  • 項目級主干開發(fā),對重構(gòu)很友好,讓重構(gòu)變得可行。(客戶端很容易實現(xiàn)主干開發(fā))。
  • 務(wù)實地講,重構(gòu)已經(jīng)不可能了。那么,你們可以謹(jǐn)慎地提出新的一整套范式。重建它。
  • 禁止 hardcode,特殊邏輯。如果你發(fā)現(xiàn)特殊邏輯容易實現(xiàn)需求,否則很難。那么,你的架構(gòu)已經(jīng)出現(xiàn)問題了,你和你的團隊?wèi)?yīng)該深入思考這個問題,而不是輕易加上一個特殊邏輯。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

為測試做設(shè)計

現(xiàn)在我們在做'測試左移',讓工程師編寫自動化測試來保證質(zhì)量。測試工程師的工作更多的是類似 google SET(Software Engineer In Test, 參考《google 軟件測試之道》)的工作。工作重心在于測試編碼規(guī)范、測試編碼流程、測試編碼工具、測試平臺的思考和建設(shè)。測試代碼,還是得工程師來做。

為方法寫一個測試的考慮過程,使我們得以從外部看待這個方法,這讓我們看起來是代碼的客戶,而不是代碼的作者。很多同學(xué),就感覺很難受。對,這是必然的。因為你的代碼設(shè)計的時候,并沒有把'容易測試'考慮進(jìn)去,可測試性不強。如果工程師在開發(fā)邏輯的過程中,就同時思考著這段代碼怎樣才能輕松地被測試。那么,這段寫就的代碼,同時可讀性、簡單性都會得到保障,經(jīng)過了良好的設(shè)計,而不僅僅是'能工作'。

我覺得,測試獲得的主要好處發(fā)生在你考慮測試及編寫測試的時候,而不是在運行測試的時候!在編碼的時候同時讓思考怎么測試的思維存在,會讓編寫高質(zhì)量的代碼變得簡單,在編碼時就更多地考慮邊界條件、異常條件,并且妥善處理。僅僅是抱有這個思維,不去真地編寫自動化測試,就能讓代碼的質(zhì)量上升,代碼架構(gòu)的能力得到提升。

硬件工程出 bug 很難查,bug 造成的成本很高,每次都要重新做一套模具、做模具的工具。所以硬件工程往往有層層測試,極早發(fā)現(xiàn)問題,盡量保證簡單且質(zhì)量高。我們可以在軟件上做同樣的事情。與硬件工程師一樣,從一開始就在軟件中構(gòu)建可測試性,并且嘗試將每個部分連接在一起之前,對他們進(jìn)行徹底的測試。

這個時候,有人就說,TDD 就是這樣,讓你同時思考編碼架構(gòu)和測試架構(gòu)。我對 TDD 的態(tài)度是: 它不一定就是最好的。測試對開發(fā)的驅(qū)動,絕對有幫助。但是,就像每次驅(qū)動汽車一樣,除非心里有一個目的地,否則就可能會兜圈子。TDD 是一種自底向上的編程方法。但是,適當(dāng)?shù)臅r候使用自頂向下設(shè)計,才能獲得一個最好的整體架構(gòu)。很多人處理不好自頂向下和自底向上的關(guān)系,結(jié)果在使用 TDD 的時候發(fā)現(xiàn)舉步維艱、收效甚微。

以及,如果沒有強大的外部驅(qū)動力,"以后再測"實際上意味著"永遠(yuǎn)不測"。大家,務(wù)實一點,在編碼時就考慮怎么測試。不然,你永遠(yuǎn)沒有機會考慮了。當(dāng)面對著測試性低的代碼,需要編寫自動化測試的時候,你會感覺很難受。

盡早測試, 經(jīng)常測試, 自動測試

一旦代碼寫出來,就要盡早開始測試。這些小魚的惡心之處在于,它們很快就會變成巨大的食人鯊,而捕捉鯊魚則相當(dāng)困難。所以我們要寫單元測試,寫很多單元測試。

事實上,好項目的測試代碼可能會比產(chǎn)品代碼更多。生成這些測試代碼所花費的時間是值得的。從長遠(yuǎn)來看,最終的成本會低得多,而且你實際上有機會生產(chǎn)出幾乎沒有缺陷的產(chǎn)品。

另外,知道通過了測試,可以讓你對代碼已經(jīng)"完成"產(chǎn)生高度信心。

項目中使用統(tǒng)一的術(shù)語

如果用戶和開發(fā)者使用不同的名稱來稱呼相同的事物,或者更糟糕的是,使用相同的名稱來代指不同的事物,那么項目就很難取得成功。

DDD(Domain-Driven Design)把'項目中使用統(tǒng)一的術(shù)語'做到了極致,要求項目把目標(biāo)系統(tǒng)分解為不同的領(lǐng)域(也可以稱作上下文)。在不同的上下文中,同一個術(shù)語名字意義可能不同,但是要項目內(nèi)統(tǒng)一認(rèn)識。比如證券這個詞,是個多種經(jīng)濟權(quán)益憑證的統(tǒng)稱,在股票、債券、權(quán)證市場,意義和規(guī)則是完全不同的。當(dāng)你第一次聽說'渦輪(港股特有金融衍生品,是一種股權(quán))'的時候,是不是瞬間蒙圈,搞不清它和證券的關(guān)系了。買'渦輪'是在買什么鬼證劵?

在軟件領(lǐng)域是一樣的。你需要對股票、債券、權(quán)證市場建模,你就得有不同的領(lǐng)域,在每個領(lǐng)域里有一套詞匯表(實體、值對象),在不同的領(lǐng)域之間,同一個概念可能會換一個名字,需要映射。如果你們既不區(qū)分領(lǐng)域,甚至在同一個領(lǐng)域還對同一個實體給出不同的名字。那,你們怎么確保自己溝通到位了?寫成代碼,別人如何知道你現(xiàn)在寫的'證券'這個 struct 具體是指的什么?

不要面向需求編程

需求不是架構(gòu);需求無關(guān)設(shè)計,也非用戶界面;需求就是需要的東西。需要的東西是經(jīng)常變化的,是不斷被探索,不斷被加深認(rèn)識的。產(chǎn)品經(jīng)理的說辭是經(jīng)常變化的。當(dāng)你面向需求編程,你就是在依賴一個認(rèn)識每一秒都在改變的女/男朋友。你將身心俱疲。

我們應(yīng)該面向業(yè)務(wù)模型編程。我在《Code Review 我都 CR 些什么》里也提到了這一點,但是我當(dāng)時并沒有給出應(yīng)該怎么去設(shè)計業(yè)務(wù)模型的指導(dǎo)。我的潛臺詞就是,你還是僅僅能憑借自己的智力和經(jīng)驗,沒有很多方法論工具。

現(xiàn)在,我給你推薦一個工具,DDD(Domain-Driven Design),面向領(lǐng)域驅(qū)動設(shè)計。它能讓你對業(yè)務(wù)更好地建模,讓對業(yè)務(wù)建模變成一個可拆解的執(zhí)行步驟,僅僅需要少得多的智力和經(jīng)驗。區(qū)分好領(lǐng)域上下文,思考明白它們之間的關(guān)系,找到領(lǐng)域下的實體和值對象,找到和模型貼合的架構(gòu)方案。這些任務(wù),讓業(yè)務(wù)建模變得簡單。

當(dāng)我們面向業(yè)務(wù)模型編程,變更的需求就變成了–提供給用戶他所需要的業(yè)務(wù)模型的不同部分。我們不再是在不斷地 change 代碼,而是在不斷地 extend 代碼,逐漸做完一個業(yè)務(wù)模型的填空題。

寫代碼要有對于'美'的追求

google 的很多同學(xué)說(至少 hankzheng 這么說),軟件工程=科學(xué) 藝術(shù)。當(dāng)前騰訊,很多人,不講科學(xué)。工程學(xué),計算機科學(xué),都不管。就喜歡搞'巧合式編程'。剛好能工作了,打完收工,交付需求。絕大多數(shù)人,根本不追求編碼、設(shè)計的藝術(shù)。對細(xì)節(jié)的好看,毫無感覺。對于一個空格、空行的使用,毫無邏輯,毫無美感。用代碼和其他人溝通,連基本的整潔、合理都不講。根本沒想過,別人會看我的代碼,我要給代碼'梳妝打扮'一下,整潔大方,美麗動人,還極有內(nèi)涵。'窈窕淑女,君子好逑',我們應(yīng)該對別人慷慨一點,你總是得閱讀別人的代碼的。大家都對美有一點追求,就是互相都慷慨一些。

很無奈,我把對美的追求說得這么'卑微'。必須要由'務(wù)實的需要'來構(gòu)建必要性。而不是每個工程師發(fā)自內(nèi)心的,像對待漂亮的異性、好的音樂、好的電影一樣的發(fā)自內(nèi)心的需要它。認(rèn)為代碼也是取悅別人、取悅自己的東西。

如果我們想做一個有尊嚴(yán)、有格調(diào)的工程師,我們就應(yīng)該把自己的代碼、勞動的產(chǎn)物,當(dāng)成一件藝術(shù)品去雕琢。務(wù)實地追求效率,同時也追求美感。效率產(chǎn)出價值,美感取悅自己。不僅僅是為了一口飯,同時也把工程師的工作當(dāng)成自己一個快樂的源頭。工作不再是 overhead,而是 happiness。此刻,你做不到,但是應(yīng)該有這樣的追求。當(dāng)我們都有了這樣的追求,有一天,我們會能像 google 一樣做到的 。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

換行

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

換行

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

換行

應(yīng)用程序框架是實現(xiàn)細(xì)節(jié)

以下是《整潔架構(gòu)之道》的原文摘抄:

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

對,DIP 大發(fā)神威。我覺得核心做法就是:

  • 核心代碼應(yīng)該通過 DIP 來讓它不要和具體框架綁定!它應(yīng)該使用 DIP(比如代理類),抽象出一個防腐層,讓自己的核心代碼免于腐壞。
  • 選擇一個框架,你不去做防腐層(主要通過 DIP),你就是單方面領(lǐng)了結(jié)婚證,你只有義務(wù),沒有權(quán)利。同學(xué)們要想明白。同學(xué)們應(yīng)該對框架本身是否優(yōu)秀,是否足夠組件化,它本身能否在項目里做到可插拔,做出思考和設(shè)計。

trpc-go 對于插件化這事兒,做得還不錯,大家會積極地使用它。trpc-cpp 離插件化非常遠(yuǎn),它自己根本就成不了一個插件,而是有一種要強暴你的感覺,你能憑直覺明顯地感覺到不愿意和它訂終身。例如,trpc-cpp 甚至強暴了你構(gòu)建、編譯項目的方式。當(dāng)然,這很多時候是 c 語言本身的問題。

‘解耦’、'插件化’就是 golang 語言的關(guān)鍵詞。大家開玩笑說,c 已經(jīng)被委員會玩壞了,加入了太多特性。less is more, more means nothing。c 從來都是讓別的工具來解決自己的問題,trpc-cpp 可能把自己松綁定到 bazel 等優(yōu)秀的構(gòu)建方案。尋求優(yōu)秀的組件去軟綁定,提供解決方案,是可行的出路。我個人喜歡 rust。但是大家還是熟悉 cpp,我們確實需要一個投入更多人力做得更好的 trpc-cpp。

一切都應(yīng)該是代碼(通過代碼去顯式組合)

Unix 編程哲學(xué)告訴我們: 如果有一些參數(shù)是可變的,我們應(yīng)該使用配置,而不是把參數(shù)寫死在代碼里。在騰訊,這一點做得很好。但是,大人,現(xiàn)在時代又變了。

J2EE 框架讓我們看到,組件也可以是通過配置 Java Bean 的形式注入到框架里的。J2EE 實現(xiàn)了把組件也配置化的壯舉。但是,時代變了!你下載一個 golang 編譯器,你進(jìn)入你下載的文件里去看,會發(fā)現(xiàn)你找不到任何配置文件。這是為什么?兩個簡單,但是很多年都被人們忽略的道理:

  • 配置即隱性耦合。配置只有和使用配置的代碼組合使用,它才能完成它的工作。它是通過把'一個事情分開兩個步驟'來換取動態(tài)性。換句話說,它讓兩個相隔十萬八千里的地方產(chǎn)生了耦合!作為工程師,你一開始就要理解雙倍的復(fù)雜度。配置如何使用、配置的處理程序會如何解讀配置。
  • 代碼能夠有很強的自解釋能力,工程師們更愿意閱讀可讀性強的代碼,而不是編寫得很爛的配置文檔。配置只能通過厚重的配置說明書去解釋。當(dāng)你缺乏完備的配置說明書,配置變成了地獄。

golang 的編譯器是怎么做的呢?它會在代碼里給你設(shè)定一個通用性較強的默認(rèn)配置項。同時,配置項都是集中管理的,就像管理配置文件一樣。你可以通過額外配置一個配置文件或者命令行參數(shù),來改變編譯器的行為。這就變成了,代碼解釋了每一個配置項是用來做什么的。只有當(dāng)你需要的時候,你會先看懂代碼,然后,當(dāng)你有需求的時候,通過額外的配置去改變一個你有預(yù)期的行為。

邏輯變成了。一開始,所有事情都是解耦的。一件事情都只看一塊代碼就能明白。代碼有較好的自解釋性和注解,不再需要費勁地編寫撇腳的文檔。當(dāng)你明白之后,你需要不一樣的行為,就通過額外的配置來實現(xiàn)。關(guān)于怎么配置,代碼里也講明白了。

對于 trpc-go 框架,以及一眾插件,優(yōu)先考慮配置,然后才是代碼去指定,部分功能還只能通過配置去指定,我就很難受。我接受它,就得把一個事情放在兩個地方去完成:

  • 需要在代碼里 import 插件包。
  • 需要在配置文件里配置插件參數(shù)。

既然不能消滅第一步,為什么不能是顯式 import,同時通過代碼 其他自定義配置管理方案去完成插件的配置?當(dāng)然,插件,直接不需要任何配置,提供代碼 Option 去改變插件的行為,是最香的。這個時候,我就真的能把 trpc 框架本身也當(dāng)成一個插件來使用了。

封裝不一定是好的組織形式

封裝(Encapsulation),是我上學(xué)時剛接觸 OOP,驚為天人的思想方法。但是,我工作了一些年頭了,看過了不知道多少腐爛的代碼。其中一部分還需要我來維護。我看到了很多莫名其妙的封裝,讓我難受至極。封裝,經(jīng)常被濫用。封裝的時候,我們一定要讓自己的代碼,自己就能解釋自己是按照下面的哪一種套路在做封裝:

  • 按層封裝
  • 按功能封裝
  • 按領(lǐng)域封裝
  • 按組件封裝

或者,其他能被命名到某種有價值的類型的封裝。你要能說出為什么你的封裝是必要的,有價值的。必要的時候,你必須要封裝。比如,當(dāng)你的 golang 函數(shù)達(dá)到了 80 行,你就應(yīng)該對邏輯分組,或者把一塊過于細(xì)節(jié)化卻功能單一的較長的代碼獨立到一個函數(shù)。同時,你又不能胡亂封裝,或者過度封裝。是否過度,取決于大家的共識,要 reviwer 能認(rèn)可你這個封裝是有價值的。當(dāng)然,你也會成為 reviewer,別人也需要獲得你的認(rèn)可。缺乏意圖設(shè)計的封裝,是破壞性的。這會使其他人在面對這段代碼時,畏首畏尾,不敢修改它。形成一個腐爛的肉塊,并且,這種腐爛會逐漸蔓延開來。

所以,所有細(xì)節(jié)都是關(guān)鍵的。每一塊磚頭都被精心設(shè)計,才能構(gòu)建一個漂亮的項目!

所有細(xì)節(jié)都應(yīng)該被顯式處理

這是一個顯而易見的道理。但是很多同學(xué)卻毫無知覺。我為需要深入閱讀他們編寫的代碼的同學(xué)默哀一秒。當(dāng)有一個函數(shù) func F() error,我僅僅是用 F(),沒有用變量接收它的返回值。你閱讀代碼的時候,你就會想,第一開發(fā)者是忘記了 error handling 了,還是他思考過了,他決定不關(guān)注這個返回值?他是設(shè)計如此,還是這里是個 bug?他人即地獄,維護代碼的苦難又多了一分。

我們對于自己的代碼可能會給別人帶來困擾的地方,都應(yīng)該顯式地去處理。就像寫了一篇不會有歧義的文章。如果就是想要忽略錯誤,'_ = F()'搞定。我將來再處理錯誤邏輯,'_ = F() // TODO 這里需要更好地處理錯誤'。在代碼里,把事情講明白,所有人都能快速理解他人的代碼,就能快速做出修改的決策。'猜測他人代碼的邏輯用意'是很難受且困難的,他人的代碼也會在這種場景下,產(chǎn)生被誤讀。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

不能上升到原則的一些常見案例

合理注釋一些并不'通俗'的邏輯和數(shù)值

和'所有細(xì)節(jié)都應(yīng)該被顯式處理'一脈相承。所有他人可能要花較多時間猜測原因的細(xì)節(jié),都應(yīng)該在代碼里提前清楚地講明白。請慷慨一點。也可能,三個月后的將來,是你回來 eat your own dog food。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

習(xí)慣留下 TODO

要這么做的道理很簡單。便于所有人能接著你開發(fā)。極有可能就是你自己接著自己開發(fā)。如果沒有標(biāo)注 TODO 把沒有做完的事情標(biāo)示出來。可能,你自己都會搞忘自己有事兒沒做完了。留下 TODO 是很簡單的事情,我們?yōu)槭裁床蛔瞿兀?/span>

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

不要丟棄錯誤信息

即'錯誤傳遞原則'。這里給它換個名字–你不應(yīng)該主動把很多有用的信息給丟棄了。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

自動化測試要快

在 google,自動化測試是硬性要求在限定時間內(nèi)跑完的。這從細(xì)節(jié)上保障了自動化測試的速度,進(jìn)而保障了自動化測試的價值和可用性。你真的需要 sleep 這么久?應(yīng)該認(rèn)真考量。考量清楚了把原因?qū)懴聛?。?dāng)大家發(fā)現(xiàn)總時長太長的時候,可以選擇其中最不必要的部分做優(yōu)化。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

歷史有問題的代碼, 發(fā)現(xiàn)了問題要及時 push 相關(guān)人主動解決

這是'控制軟件的熵是軟件工程的重要任務(wù)之一'的表現(xiàn)之一。我們是團隊作戰(zhàn),不是無組織無記錄的部隊。發(fā)現(xiàn)了問題,就及時拋出和解決。讓傷痛更少,跑得更快。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

less is more

less is more. 《Code Review 我都 CR 些什么》強調(diào)過了,這里不再強調(diào)。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

less is more

如果打了錯誤日志, 有效信息必須充足, 且不過多

和'less is more'一脈相承。同時,必須有的時候,就得有,不能漏。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

日志

注釋要把問題講清楚, 講不清楚的日志等于沒有

是個簡單的道理,和'所有細(xì)節(jié)都應(yīng)該被顯式處理'一脈相承。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

日志

MR 要自己先 review, 不要浪費 reviewer 的時間

你也會成為 reviewer,節(jié)省他人的時間,他人也節(jié)省你的時間??s短交互次數(shù),提升 review 的愉悅感。讓他人提的 comment 都是'言之有物'的東西,而不是一些反反復(fù)復(fù)的最基礎(chǔ)的細(xì)節(jié)。會讓他人更愉悅,自己在看 comment 的時候,也更愉悅,更愿意去討論、溝通。讓 code review 成為一個技術(shù)交流的平臺。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

時間

要尋找合適的定語

這個顯而易見。但是,同學(xué)們就是愛放縱自己?

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

定語

不要出現(xiàn)特定 IP,或者把什么可變的東西寫死

這個和'ETC'一脈相承,我覺得也是顯而易見的東西。但是很多同學(xué)還是喜歡放縱自己?

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

寫死

使用定語, 不要 1、2、3、4

這個存粹就是放縱自己了。當(dāng)然,也會有只能用 1、2、3、4 的時候。但是,你這里,是么?多數(shù)時候,都不會是。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

數(shù)字

有必要才使用 init

這,也顯而易見。init 很方便,但是,它也會帶來心智負(fù)擔(dān)。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

init

要關(guān)注 shadow write

這個很重要,看例子就知道了。但是大家常常忽略,特此提一下。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

shadow

能不耦合接收器就別耦合

減少耦合是我們保障代碼質(zhì)量的重要手段。請把 ETC 原則放在自己的頭上漂浮著,時刻帶著它思考,不要懶惰。熟能生巧,它并不會成為心智負(fù)擔(dān)。反而常常會在你做決策的時候幫你快速找到方向,提升決策速度。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

接收器

空實現(xiàn)需要注明空實現(xiàn)就是實現(xiàn)

這個和'所有細(xì)節(jié)都應(yīng)該被顯式處理'一脈相承。這個理念,我見過無數(shù)種形式表現(xiàn)出來。這里就是其中一種。列舉這個 case,讓你印象再深刻一點。

程序員進(jìn)階指南:文檔團隊Golang最佳實踐和CR案例集分享(程序員文檔工具)

空實現(xiàn)

看錯題集沒多少有用, 我們需要教練和傳承

上面我列了很多例子。是我能列出來的例子中的九牛一毛。但是,我列一個非常龐大的錯題集沒有任何用。我也不再例舉更多。只有當(dāng)大家信奉了敏捷工程的美。認(rèn)可好的代碼架構(gòu)對于業(yè)務(wù)的價值,才能真正地做到舉一反三,理解無數(shù)例子,能對更多的 case 自己做出合理的判斷。同時,把好的判斷傳播起來,做到"群體免疫",最終做好 review,做好代碼質(zhì)量。

展望

希望本文能幫助到需要做好 CR、做好編碼,需要培養(yǎng)更多 reviwer 的團隊。讓你門看到很多原則,吸收這些原則和理念。去理解、相信這些理念。在 CR 中把這些理念、原則傳播出去。成為別人的臨時教練,讓大家都成為合格的 reviwer。加強對于代碼的交流,飛輪效應(yīng),讓團隊構(gòu)建好的人才梯度和工程文化。

寫到最后,我發(fā)現(xiàn),我上面寫的這些東西都不那么重要了。你有想把代碼寫得更利于團隊協(xié)作的價值觀和態(tài)度,反而是最重要的事情。上面講的都僅僅是寫高質(zhì)量代碼的手段和思想方法。當(dāng)你認(rèn)可了'應(yīng)該編寫利于團隊協(xié)作的高質(zhì)量代碼',并且擁有對'不利于團隊代碼質(zhì)量的代碼'嫉惡如仇的態(tài)度。你總能找到高質(zhì)量代碼的寫法。沒有我?guī)湍憧偨Y(jié),你也總會掌握!

拾遺

如果你深入了解 DDD,就會了解到'六邊形架構(gòu)'、'CQRS(Command Query Responsibility Segregation,查詢職責(zé)分離)架構(gòu)'、'事件驅(qū)動架構(gòu)'等關(guān)鍵詞。這是 DDD 構(gòu)建自己體系的基石,這些架構(gòu)及是細(xì)節(jié)又是頂層設(shè)計,也值得了解一下。

版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶自發(fā)貢獻(xiàn),該文觀點僅代表作者本人。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請發(fā)送郵件至 舉報,一經(jīng)查實,本站將立刻刪除。