作者: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é)起來就是,解釋信息必須離被解釋的東西,越近越好。代碼能做到自解釋,是最棒的。
讓目錄結(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ù)!
正交性原則(全局變量的危害)
'正交性'是幾何學(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ù)雜、隱秘、難以定位。
全局 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,那么,你的代碼就是可逆性做得很差。書里有一個例證,我覺得很好,直接引用過來。
與其認(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)。
從這個依賴圖觀察。我們發(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ù)案:
解耦代碼讓改變?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)試信息,對其他人根本沒有價值。
- 即使低級別日志,也不能泛濫。不然,日志打開與否都沒有差別,日志變得毫無價值。
緘默
錯誤傳遞原則
我不喜歡 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 客戶端。至少不能直接展示給用戶看到。
錯誤鏈
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ù)實的程序員。
控制軟件的熵是軟件工程的重要任務(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)該深入思考這個問題,而不是輕易加上一個特殊邏輯。
為測試做設(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 一樣做到的 。
換行
換行
換行
應(yīng)用程序框架是實現(xiàn)細(xì)節(jié)
以下是《整潔架構(gòu)之道》的原文摘抄:
對,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)生被誤讀。
不能上升到原則的一些常見案例
合理注釋一些并不'通俗'的邏輯和數(shù)值
和'所有細(xì)節(jié)都應(yīng)該被顯式處理'一脈相承。所有他人可能要花較多時間猜測原因的細(xì)節(jié),都應(yīng)該在代碼里提前清楚地講明白。請慷慨一點。也可能,三個月后的將來,是你回來 eat your own dog food。
習(xí)慣留下 TODO
要這么做的道理很簡單。便于所有人能接著你開發(fā)。極有可能就是你自己接著自己開發(fā)。如果沒有標(biāo)注 TODO 把沒有做完的事情標(biāo)示出來。可能,你自己都會搞忘自己有事兒沒做完了。留下 TODO 是很簡單的事情,我們?yōu)槭裁床蛔瞿兀?/span>
不要丟棄錯誤信息
即'錯誤傳遞原則'。這里給它換個名字–你不應(yīng)該主動把很多有用的信息給丟棄了。
自動化測試要快
在 google,自動化測試是硬性要求在限定時間內(nèi)跑完的。這從細(xì)節(jié)上保障了自動化測試的速度,進(jìn)而保障了自動化測試的價值和可用性。你真的需要 sleep 這么久?應(yīng)該認(rèn)真考量。考量清楚了把原因?qū)懴聛?。?dāng)大家發(fā)現(xiàn)總時長太長的時候,可以選擇其中最不必要的部分做優(yōu)化。
歷史有問題的代碼, 發(fā)現(xiàn)了問題要及時 push 相關(guān)人主動解決
這是'控制軟件的熵是軟件工程的重要任務(wù)之一'的表現(xiàn)之一。我們是團隊作戰(zhàn),不是無組織無記錄的部隊。發(fā)現(xiàn)了問題,就及時拋出和解決。讓傷痛更少,跑得更快。
less is more
less is more. 《Code Review 我都 CR 些什么》強調(diào)過了,這里不再強調(diào)。
less is more
如果打了錯誤日志, 有效信息必須充足, 且不過多
和'less is more'一脈相承。同時,必須有的時候,就得有,不能漏。
日志
注釋要把問題講清楚, 講不清楚的日志等于沒有
是個簡單的道理,和'所有細(xì)節(jié)都應(yīng)該被顯式處理'一脈相承。
日志
MR 要自己先 review, 不要浪費 reviewer 的時間
你也會成為 reviewer,節(jié)省他人的時間,他人也節(jié)省你的時間??s短交互次數(shù),提升 review 的愉悅感。讓他人提的 comment 都是'言之有物'的東西,而不是一些反反復(fù)復(fù)的最基礎(chǔ)的細(xì)節(jié)。會讓他人更愉悅,自己在看 comment 的時候,也更愉悅,更愿意去討論、溝通。讓 code review 成為一個技術(shù)交流的平臺。
時間
要尋找合適的定語
這個顯而易見。但是,同學(xué)們就是愛放縱自己?
定語
不要出現(xiàn)特定 IP,或者把什么可變的東西寫死
這個和'ETC'一脈相承,我覺得也是顯而易見的東西。但是很多同學(xué)還是喜歡放縱自己?
寫死
使用定語, 不要 1、2、3、4
這個存粹就是放縱自己了。當(dāng)然,也會有只能用 1、2、3、4 的時候。但是,你這里,是么?多數(shù)時候,都不會是。
數(shù)字
有必要才使用 init
這,也顯而易見。init 很方便,但是,它也會帶來心智負(fù)擔(dān)。
init
要關(guān)注 shadow write
這個很重要,看例子就知道了。但是大家常常忽略,特此提一下。
shadow
能不耦合接收器就別耦合
減少耦合是我們保障代碼質(zhì)量的重要手段。請把 ETC 原則放在自己的頭上漂浮著,時刻帶著它思考,不要懶惰。熟能生巧,它并不會成為心智負(fù)擔(dān)。反而常常會在你做決策的時候幫你快速找到方向,提升決策速度。
接收器
空實現(xiàn)需要注明空實現(xiàn)就是實現(xiàn)
這個和'所有細(xì)節(jié)都應(yīng)該被顯式處理'一脈相承。這個理念,我見過無數(shù)種形式表現(xiàn)出來。這里就是其中一種。列舉這個 case,讓你印象再深刻一點。
空實現(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)查實,本站將立刻刪除。