萬字詳文闡釋程式設計師修煉之道

騰訊技術工程發表於2020-12-08

作者:cheaterlin,騰訊 PCG 後臺開發工程師

綜述

我寫過一篇《Code Review 我都 CR 些什麼》,講解了 Code Review 對團隊有什麼價值,我認為 CR 最重要的原則有哪些。最近我在團隊工作中還發現了:

  • 原則不清晰。對於程式碼架構的原則,編碼的追求,我的骨幹員工對它的認識也不是很全面。當前還是在 review 過程中我對他們口口相傳,總有遺漏。
  • 從知道到會做需要時間。我需要反覆跟他們補充 review 他們漏掉的點,他們才能完成吸收、內化,在後續的 review 過程中,能自己提出這些 review 的點。

過度文件化是有害的,當過多的內容需要被閱讀,工程師們最終就會選擇不去讀,讀了也僅僅能吸收很少一部分。在 google,對於程式碼細節的理解,更多還是口口相傳,在實踐中去感受和理解。但是,適當的文件、文字宣傳,是必要的。特此,我就又輸出了這一篇文章,嘗試從'知名架構原則'、'工程師的自我修養'、'不能上升到原則的幾個常見案例'三大模組,把我個人的經驗系統地輸出,供其他團隊參考。

知名架構原則

後面原則主要受《程式設計師修煉之道: 通向務實的最高境界》、《架構整潔之道》、《Unix 程式設計藝術》啟發。我不是第一個發明這些原則的人,甚至不是第一個總結出來的人,別人都已經寫成書了!務實的程式設計師對於方法的總結,總是殊途同歸。

細節即是架構

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

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

但是,如果檢視建築設計師使用的圖紙,會發現其中也充斥著大量的設計細節。譬如,我們可以看到每個插座、開關以及每個電燈具體的安裝位置,同時也可以看到某個開關與所控制的電燈的具體連線資訊;我們也能看到壁爐的具體位置,熱水器的大小和位置資訊,甚至是汙水泵的位置;同時也可以看到關於牆體、屋頂和地基所有非常詳細的建造說明。總的來說,架構圖裡實際上包含了所有的底層設計細節,這些細節資訊共同支撐了頂層的架構設計,底層設計資訊和頂層架構設計共同組成了整個房屋的架構文件。

軟體設計也是如此。底層設計細節和高層架構資訊是不可分割的。他們組合在一起,共同定義了整個軟體系統,缺一不可。所謂的底層和高層本身就是一系列決策組成的連續體,並沒有清晰的分界線。

我們編寫、review 細節程式碼,就是在做架構設計的一部分。我們編寫的細節程式碼構成了整個系統。我們就應該在細節 review 中,總是帶著所有架構原則去審視。你會發現,你已經寫下了無數讓整體變得醜陋的細節,它們背後,都有前人總結過的架構原則。

把程式碼和文件綁在一起(自解釋原則)

寫文件是個好習慣。但是寫一個別人需要諮詢老開發者才能找到的文件,是個壞習慣。這個壞習慣甚至會給工程師們帶來傷害。比如,當初始開發者寫的文件在一個犄角旮旯(在 wiki 裡,但是閱讀程式碼的時候沒有在明顯的位置看到連結),後續程式碼被修改了,文件已經過時,有人再找出文件來獲取到過時、錯誤的知識的時候,閱讀文件這個同學的開發效率必然受到傷害。所以,如同 golang 的 godoc 工具能把程式碼裡'按規範來'的註釋自動生成一個文件頁面一樣,我們應該:

  • 按照 godoc 的要求好好寫程式碼的註釋。
  • 程式碼首先要自解釋,當解釋不了的時候,需要就近、合理地寫註釋。
  • 當小段的註釋不能解釋清楚的時候,應該有 doc.go 來解釋,或者,在同級目錄的 ReadMe.md 裡註釋講解。
  • 文件需要強大的富文字編輯能力,Down 無法滿足,可以寫到 wiki 裡,同時必須把 wiki 的簡單描述和連結放在程式碼裡合適的位置。讓閱讀和維護程式碼的同學一眼就看到,能做到及時的維護。

以上,總結起來就是,解釋資訊必須離被解釋的東西,越近越好。程式碼能做到自解釋,是最棒的。

萬字詳文闡釋程式設計師修煉之道讓目錄結構自解


ETC 價值觀(easy to change)

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

敏捷軟體工程,所謂敏捷,就是要能快速變更,並且在變更中保持程式碼的質量。所以,持有 ETC 價值觀看待程式碼細節、技術方案,我們將能更好地編寫出適合敏捷專案的程式碼。這是一個大的價值觀,不是一個基礎微觀的原則,所以沒有例子。本文提到的所有原則,或者接,或間接,都要為 ETC 服務。

DRY 原則(don not repeat yourself)

在《Code Review 我都 CR 些什麼》裡面,我已經就 DRY 原則做了深入闡述,這裡不再贅述。我認為 DRY 原則是編碼原則中最重要的編碼原則,沒有之一(ETC 是個觀念)。不要重複!不要重複!不要重複!

萬字詳文闡釋程式設計師修煉之道

正交性原則(全域性變數的危害)

'正交性'是幾何學中的術語。我們的程式碼應該消除不相關事物之間的影響。這是一給簡單的道理。我們寫程式碼要'高內聚、低耦合',這是大家都在提的。

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

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

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

萬字詳文闡釋程式設計師修煉之道

單例就是全域性變數

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

可逆性原則

可逆性原則是很少被提及的一個原則。可逆性,就是你做出的判斷,最好都是可以被逆轉的。再換一個容易懂的說法,你最好儘量少認為什麼東西是一定的、不變的。比如,你認為你的系統永遠服務於,用 32 位無符號整數(比如 QQ 號)作為使用者標識的系統。你認為,你的持久化儲存,就選型 SQL 儲存了。當這些一開始你認為一定的東西,被推翻的時候,你的程式碼卻很難去 change,那麼,你的程式碼就是可逆性做得很差。書裡有一個例證,我覺得很好,直接引用過來。

萬字詳文闡釋程式設計師修煉之道

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

我今天才想起一個事兒。當年 SNG 的很多部門對於 metrics 監控的使用。就潛意識地認為,我們將一直使用'模組間呼叫監控'元件。使用它的 API 是直接把上報通道 DCLog 的 API 裸露在業務程式碼裡的。今天(2020.12.01),該元件應該已經完全沒有人維護、完全下線了,這些核心業務程式碼要怎麼辦?有人能對它做出修改麼?那,這些部門現在還有 metrics 監控麼?答案,可能是悲觀的。有人已經已經嚐到了可逆性之痛。

依賴倒置原則(DIP)

DIP 原則太重要了,我這裡單獨列一節來講解。我這裡只是簡單的講解,講解它最原始和簡單的形態。依賴倒置原則,全稱是 Dependence Inversion Principle,簡稱 DIP。考慮下面這幾段程式碼:

package dip

package dip

type 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 javaimpl

import "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 pythonimpl

import "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 main

import (
    "javaimpl"
    "pythonimpl"
    "dip"
)

func runPoll(b dip.Botton) {
    ui := NewUI(b)
    ui.Poll()
}

func main() {
    runPoll(pythonimpl.NewLamp())
    runPoll(javaimpl.NewLamp())
}

看程式碼,main pkg 裡的 runPoll 函式僅僅面向 Botton interface 編碼,main pkg 不再關心 Botton interface 裡定義的 TurnOn、TurnOff 的實現細節。實現瞭解耦。這裡,我們能看到 struct UI 需要被注入(inject)一個 Botton interface 才能邏輯完整。所以,DIP 經常換一個名字出現,叫做依賴注入(Dependency Injection)。

萬字詳文闡釋程式設計師修煉之道

從這個依賴圖觀察。我們發現,一般來說,UI struct 的實現是要應該依賴於具體的 pythonLamp、javaLamp、其他各種 Lamp,才能讓自己的邏輯完整。那就是 UI struct 依賴於各種 Lamp 的實現,才能邏輯完整。但是,我們看上面的程式碼,卻是反過來了。pythonLamp、javaLamp、其他各種 Lamp 是依賴 Botton interface 的定義,才能用來和 UI struct 組合起來拼接成完整的業務邏輯。變成了,Lamp 的實現細節,依賴於 UI struct 對於 Botton interface 的定義。這個時候,你發現,這種依賴關係被倒置了!依賴倒置原則裡的'倒置',就是這麼來的。在 golang 裡,'pythonLamp、javaLamp、其他各種 Lamp 是依賴 Botton interface 的定義',這個依賴是隱性的,沒有顯式的 implement 和 extend 關鍵字。程式碼層面,pkg dip 和 pkg pythonimpl、javaimpl 沒有任何依賴關係。他們僅僅需要被你在 main pkg 裡組合起來使用。

在 J2EE 裡,使用者的業務邏輯不再依賴低具體低層的各種儲存細節,而僅僅依賴一套配置化的 Java Bean 介面。Object 落地儲存的具體細節,被做成了 Java Bean 配置,注入到框架裡。這就是 J2EE 的核心科技,並不複雜,其實也沒有多麼'高不可攀'。反而,在'動態程式碼'優於'配置'的今天,這種透過配置實現的依賴注入,反而有點過時了。

將知識用純文字來儲存

這也是一個生僻的原則。指程式碼操作的資料和方案設計文稿,如果沒有充分的必要使用特定的方案,就應該使用人類可讀的文字來儲存、互動。對於方案設計文稿,你能不使用 office 格式,就不使用(office 能極大提升效率,才用),最好是原始 text。這是《Unix 程式設計藝術》也提到了的 Unix 系產生的設計信條。簡而言之一句話,當需要確保有一個所有各方都能使用的公共標準,才能實現互動溝通時,純文字就是這個標準。它是一個接受度最高的通行標準。如果沒有必要的理由,我們就應該使用純文字。

契約式設計

如果你對契約式設計(Design by Contract, DBC)還很陌生,我相信,你和其他端的同學(web、client、後端)聯調需求應該是一件很花費時間的事情。你自己編寫介面自動化,也會是一件很耗費精力的事情。你先看看它的wiki 解釋吧。grpc + grpc-gateway + swagger 是個很香的東西。

程式碼是否不多不少剛好完成它宣稱要做的事情,可以使用契約加以校驗和文件化。TDD 就是全程在不斷調整和履行著契約。TDD(Test-Driven Development)是自底向上地編碼過程,其實會耗費大量的精力,並且對於一個良好的層級架構沒有幫助。TDD 不是強推的規範,但是同學們可以用一用,感受一下。TDD 方法論實現的介面、函式,自我解釋能力一般來說比較強,因為它就是一個實現契約的過程。

拋開 TDD 不談。我們的函式、api,你能快速抓住它描述的核心契約麼?它的契約簡單麼?如果不能、不簡單,那你應該要求被 review 的程式碼做出調整。如果你在指導一個後輩,你應該幫他思考一下,給出至少一個可能的簡化、拆解方向。

儘早崩潰

Erlang 和 Elixir 語言信奉這種哲學。喬-阿姆斯特朗,Erlang 的發明者,《Erlang 程式設計》的作者,有一句反覆被引用的話: "防禦式程式設計是在浪費時間,讓它崩潰"。

儘早崩潰不是說不容錯,而是程式應該被設計成允許出故障,有適當的故障監管程式和程式碼,及時告警,告知工程師,哪裡出問題了,而不是嘗試掩蓋問題,不讓程式設計師知道。當最後程式設計師知道程式出故障的時候,已經找不到問題出現在哪裡了。

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

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

萬字詳文闡釋程式設計師修煉之道

解耦程式碼讓改變容易

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

  • 不相關的 pkg 之間古怪的依賴關係
  • 對一個模組進行的'簡單'修改,會傳播到系統中不相關的模組裡,或是破壞了系統中的其他部分
  • 開發人員害怕修改程式碼,因為他們不確定會造成什麼影響
  • 會議要求每個人都必須參加,因為沒有人能確定誰會受到變化的影響

只管命令不要詢問

看看如下三段程式碼:

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)
}

明顯,最後一段程式碼最簡潔。不關心 Orders 成員、總價的存在,直接命令 customer 找到 Order 並對其進行打折。當我們調整 Orders 成員、GetTotals()方法的時候,這段程式碼不用修改。還有一種更嚇人的寫法:

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

它做了更多的查詢,關心了更多的細節,變得更加 hard to change 了。我相信,大家寫過類似的程式碼也不少。特別是客戶端同學。

最好的那一段程式碼,就是隻管給每個 struct 傳送命令,要求大家做事兒。怎麼做,就內聚在和 struct 關聯的方法裡,其他人不要去操心。一旦其他人操心了,當需要做修改的時候,就要操心了這個細節的人都一起參與進修改過程。

不要鏈式呼叫方法

看下面的例子:

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

第二個例子明顯優於第一個,它變得更簡單、通用、ETC。我們應該給函式傳入它關心的最小集合作為引數。而不是,我有一個 struct,當某個函式需要這個 struct 的成員的時候,我們把整個 struct 都作為引數傳遞進去。應該僅僅傳遞函式關心的最小集合。傳進去的一整條呼叫鏈對函式來說,都是無關的耦合,只會讓程式碼更 hard to change,讓工程師懼怕去修改。這一條原則,和上一條關係很緊密,問題常常同時出現。還是,特別是在客戶端程式碼裡。

繼承稅(多用組合)

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

因此,即使你很喜歡複雜的型別樹,也完全無法為你的領域準確地建模。

Java 下一切都是類。C++裡不使用類還不如使用 C。寫 Python、PHP,我們也肯定要時髦地寫一些類。寫類可以,當你要去繼承,你就得考慮清楚了。繼承樹一旦形成,就是非常 hard to change 的,在敏捷專案裡,你要想清楚'代價是什麼',有必要麼?這個設計'可逆'麼?對於邊界清晰的 UI 框架、遊戲引擎,使用複雜的繼承樹,挺好的。對於 UI 邏輯、後臺邏輯,可能,你僅僅需要組合、DIP(依賴反轉)技術、契約式程式設計(介面與協議)就夠了。寫出繼承樹不是'就應該這麼做',它是成本,繼承是要收稅的!

在 golang 下,繼承稅的煩惱被減輕了,golang 從來說自己不是 OO 的語言,但是你 OO 的事情,我都能輕鬆地做到。更進一步,OO 和程式式程式設計的區別到底是什麼?

程式導向,物件導向,函數語言程式設計。三種程式設計結構的核心區別,是在不同的方向限制程式設計師,來做到好的程式碼結構(引自《架構整潔之道》):

  • 結構化程式設計是對程式控制權的直接轉移的限制。
  • 物件導向是對程式控制權的間接轉移的限制。
  • 函數語言程式設計是對程式中賦值操作的限制。

SOLID 原則(單一功能、開閉原則、里氏替換、介面隔離、依賴反轉,後面會講到)是 OOP 程式設計的最經典的原則。其中 D 是指依賴倒置原則(Dependence Inversion Principle),我認為,是 SOLID 裡最重要的原則。J2EE 的 container 就是圍繞 DIP 原則設計的。DIP 能用於避免構建複雜的繼承樹,DIP 就是'限制控制權的間接轉移'能繼續發揮積極作用的最大保障。合理使用 DIP 的 OOP 程式碼才可能是高質量的程式碼。

golang 的 interface 是 duck interface,把 DIP 原則更進一步,不需要顯式 implement/extend interface,就能做到 DIP。golang 使用結構化程式設計正規化,卻有物件導向程式設計正規化的核心優點,甚至簡化了。這是一個基於高度抽象理解的極度精巧的設計。google 把 abstraction 這個設計理念發揮到了極致。曾經,J2EE 的 container(EJB, Java Bean)設計是國內 Java 程式設計師引以為傲'架構設計'、'厲害的設計'。

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

共享狀態是不正確的狀態

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

問題出在共享狀態。餐廳裡的每一個服務員都檢視了陳列櫃,卻沒有考慮到其他服務員。你們可以透過加互斥鎖來解決正確性的問題,但是,兩個顧客有一個會失望或者很久都得不到答案,這是肯定的。

所謂共享狀態,換個說法,就是: 由多個人檢視和修改狀態。這麼一說,更好的解決方案就浮出水面了: 將狀態改為集中控制。預定蘋果派,不再是先查詢,再下單。而是有一個餐廳經理負責和服務員溝通,服務員只管傳送下單的命令/訊息,經理看情況能不能滿足服務員的命令。

這種解決方案,換一個說法,也可以說成"用角色實現併發性時不必共享狀態"。對,上面,我們引入了餐廳經理這個角色,賦予了他職責。當然,我們僅僅應該給這個角色傳送命令,不應該去詢問他。前面講過了,'只管命令不要詢問',你還記得麼。

同時,這個原則就是 golang 裡大家耳熟能詳的諺語: "不要透過共享記憶體來通訊,而應該透過通訊來共享記憶體"。作為併發性問題的根源,記憶體的共享備受關注。但實際上,在應用程式程式碼共享可變資源(檔案、資料庫、外部服務)的任何地方,問題都有可能冒出來。當程式碼的兩個或多個例項可以同時訪問某些資源時,就會出現潛在的問題。

緘默原則

如果一個程式沒什麼好說,就保持沉默。過多的正常日誌,會掩蓋錯誤資訊。過多的資訊,會讓人根本不再關注新出現的資訊,'更多資訊'變成了'沒有資訊'。每人新增一點資訊,就變成了輸出很多資訊,最後等於沒有任何資訊。

  • 不要在正常 case 下列印日誌。
  • 不要在單元測試裡使用 fmt 標準輸出,至少不要提交到 master。
  • 不打不必要的日誌。當錯誤出現的時候,會非常明顯,我們能第一時間反應過來並處理。
  • 讓除錯的日誌停留在除錯階段,或者使用較低的日誌級別,你的除錯資訊,對其他人根本沒有價值。
  • 即使低階別日誌,也不能氾濫。不然,日誌開啟與否都沒有差別,日誌變得毫無價值。
萬字詳文闡釋程式設計師修煉之道

錯誤傳遞原則

我不喜歡 Java 和 C++的 exception 特性,它容易被濫用,它具有傳染性(如果程式碼 throw 了 excepttion, 你就得 handle 它,不 handle 它,你就崩潰了。可能你不希望崩潰,你僅僅希望報警)。但是 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++規範在常規情況禁用 exception,理由包含如下內容:

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 和微軟的文章中,我們不難總結出以下幾點衍生的結論:

  • 在必要的時候丟擲 exception。使用者必須具備'必要性'的判斷能力。
  • exception 能一路把底層的異常往上傳遞到高函式層級,資訊被向上傳遞,並且在上級被妥善處理。可以讓異常和關心具體異常的處理函式在高層級和低層級遙相呼應,中間層級什麼都不需要做,僅僅向上傳遞。
  • exception 傳染性很強。當程式碼由多人協作,使用 A 模組的程式碼都必須要了解它可能丟擲的異常,做出合理的處理。不然,就都寫一個醜陋的 catch,catch 所有異常,然後做一個沒有針對性的處理。每次 catch 都需要加深一個程式碼層級,程式碼常常寫得很醜。

我們看到了異常的優缺點。上面第二點提到的資訊傳遞,是很有價值的一點。golang 在 1.13 版本中擴充了標準庫,支援了Error Wrapping也是承認了 error 傳遞的價值。

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

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

萬字詳文闡釋程式設計師修煉之道

SOLID

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

  • SRP: 單一職責原則
  • OCP: 開閉原則
  • LSP: 里氏替換原則
  • ISP: 介面隔離原則
  • DIP: 依賴反轉原則

這些年來,這幾個設計原則在很多不同的出版物裡都有過詳細描述。它們太出名了,我這裡就不更多地做詳解了。我這裡想說的是,這 5 個原則環環相扣,前 4 個原則,要麼就是同時做到,要麼就是都沒做到,很少有說,做到其中一點其他三點都不滿足。ISP 就是做到 LSP 的常用手段。ISP 也是做到 DIP 的基礎。只是,它剛被提出來的時候,是主要針對'設計繼承樹'這個目的的。現在,它們已經被更廣泛地使用在模組、領域、元件這種更大的概念上。

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

要做到 OCP 開閉原則,其實,就是要大家要透過後面講到的'不要面向需求程式設計'才能做好。如果你還是面向需求、面向 UI、互動程式設計,你永遠做不到開閉,並且不知道如何才能做到開閉。

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

一個函式不要出現多個層級的程式碼

// 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中拿到關鍵的登陸態等有效資訊
 var session common.BaseSession
 common.GetBaseSessionFromCookie(ctx, &session)
 // 校驗登陸態
 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 寫回包的程式碼,其他部分都是頂層函式組合出來的。閱讀程式碼,我們不會掉到細節裡出不來,反而忽略了整個業務流程。同時,我們能明顯發現它沒寫完,以及 common.ErrorResponse 和 defer func 兩個地方都寫了回包,可能出現發起兩次 http 回包。TODO 也會非常顯眼。

想象一下,我們沒有把細節收歸進 checkCookie()、getRelationship()等函式,而是展開在這裡,但是總函式行數沒有到 80 行,表面上符合規範。但是實際上,閱讀程式碼的同學不再能輕鬆掌握業務邏輯,而是同時在閱讀功能細節和業務流程。閱讀程式碼變成了每個時刻心智負擔都很重的事情。

顯而易見,單個函式里應該只保留某一個層級(layer)的程式碼,更細化的細節應該被抽象到下一個 layer 去,成為子函式。

Unix 哲學基礎

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

  • 模組原則: 使用簡潔的介面拼合簡單的部件
  • 清晰原則: 清晰勝於技巧
  • 組合原則: 設計時考慮拼接組合
  • 分離原則: 策略同機制分離,介面同引擎分離
  • 簡潔原則: 設計要簡潔,複雜度能低則低
  • 吝嗇原則: 除非確無它法,不要編寫龐大的程式
  • 透明性原則: 設計要可見,以便審查和除錯
  • 健壯原則: 健壯源於透明與簡潔
  • 表示原則: 把知識疊入資料以求邏輯質樸而健壯
  • 通俗原則: 介面設計避免標新立異
  • 緘默原則: 如果一個程式沒什麼好說,就保持沉默
  • 補救原則: 出現異常時,馬上退出並給出足量錯誤資訊
  • 經濟原則: 寧花機器一分,不花程式設計師一秒
  • 生成原則: 避免手工 hack,儘量編寫程式去生成程式
  • 最佳化原則: 雕琢前先得有原型,跑之前先學會走
  • 多樣原則: 絕不相信所謂"不二法門"的斷言
  • 擴充套件原則: 設計著眼未來,未來總比預想快

工程師的自我修養

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

偏執

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

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

萬字詳文闡釋程式設計師修煉之道

控制軟體的熵是軟體工程的重要任務之一

熵是個物理學概念,大家可能看過諾蘭的電影《信條》。簡單來說,熵可以理解為'混亂程度'。我們的專案,在剛開始的幾千行程式碼,是很簡潔的。但是,為什麼到了 100w 行,我們常常就感覺'太複雜了'?比如 QQ 客戶端,最近終於在做大面積重構,但是發現無數 crash。其中一個重要原因,就是'混亂程度'太高了。'混亂程度',理解起來還是比較抽象,它有很多其他名字。'hard code 很多'、'特殊邏輯很多'、'定製化邏輯很多'。再換另一個抽象的說法,'我們面對一類問題,採取了過多的正規化和特殊邏輯細節去實現它'。

熵,是一點點堆疊起來的,在一個需求的 2000 行程式碼更改中,你可能就引入了一個不同的正規化,打破了之前的通用正規化。在微觀來看,你覺得你的程式碼是'整潔乾淨'的。就像一個已經穿著好看的紅色風衣的人,你隔一天讓他接著穿上一條綠色的褲子,這還乾淨整潔麼?熵,在不斷增加,我們需要做到以下幾點,不然你的團隊將在希望透過重構來降低專案的熵的時候嚐到惡果,甚至放棄重構,讓熵不斷增長下去。

  • 如果沒有充分的理由,始終使用專案規範的正規化對每一類問題做出解決方案。
  • 如果業務發展發現老的解決方案不再優秀,做整體重構。
  • 專案級主幹開發,對重構很友好,讓重構變得可行。(客戶端很容易實現主幹開發)。
  • 務實地講,重構已經不可能了。那麼,你們可以謹慎地提出新的一整套正規化。重建它。
  • 禁止 hardcode,特殊邏輯。如果你發現特殊邏輯容易實現需求,否則很難。那麼,你的架構已經出現問題了,你和你的團隊應該深入思考這個問題,而不是輕易加上一個特殊邏輯。
萬字詳文闡釋程式設計師修煉之道
萬字詳文闡釋程式設計師修煉之道

為測試做設計

現在我們在做'測試左移',讓工程師編寫自動化測試來保證質量。測試工程師的工作更多的是類似 google SET(Software Engineer In Test, 參考《google 軟體測試之道》)的工作。工作重心在於測試編碼規範、測試編碼流程、測試編碼工具、測試平臺的思考和建設。測試程式碼,還是得工程師來做。

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

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

硬體工程出 bug 很難查,bug 造成的成本很高,每次都要重新做一套模具、做模具的工具。所以硬體工程往往有層層測試,極早發現問題,儘量保證簡單且質量高。我們可以在軟體上做同樣的事情。與硬體工程師一樣,從一開始就在軟體中構建可測試性,並且嘗試將每個部分連線在一起之前,對他們進行徹底的測試。

這個時候,有人就說,TDD 就是這樣,讓你同時思考編碼架構和測試架構。我對 TDD 的態度是: 它不一定就是最好的。測試對開發的驅動,絕對有幫助。但是,就像每次驅動汽車一樣,除非心裡有一個目的地,否則就可能會兜圈子。TDD 是一種自底向上的程式設計方法。但是,適當的時候使用自頂向下設計,才能獲得一個最好的整體架構。很多人處理不好自頂向下和自底向上的關係,結果在使用 TDD 的時候發現舉步維艱、收效甚微。

以及,如果沒有強大的外部驅動力,"以後再測"實際上意味著"永遠不測"。大家,務實一點,在編碼時就考慮怎麼測試。不然,你永遠沒有機會考慮了。當面對著測試性低的程式碼,需要編寫自動化測試的時候,你會感覺很難受。

儘早測試, 經常測試, 自動測試

一旦程式碼寫出來,就要儘早開始測試。這些小魚的噁心之處在於,它們很快就會變成巨大的食人鯊,而捕捉鯊魚則相當困難。所以我們要寫單元測試,寫很多單元測試。

事實上,好專案的測試程式碼可能會比產品程式碼更多。生成這些測試程式碼所花費的時間是值得的。從長遠來看,最終的成本會低得多,而且你實際上有機會生產出幾乎沒有缺陷的產品。

另外,知道透過了測試,可以讓你對程式碼已經"完成"產生高度信心。

專案中使用統一的術語

如果使用者和開發者使用不同的名稱來稱呼相同的事物,或者更糟糕的是,使用相同的名稱來代指不同的事物,那麼專案就很難取得成功。

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

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

不要面向需求程式設計

需求不是架構;需求無關設計,也非使用者介面;需求就是需要的東西。需要的東西是經常變化的,是不斷被探索,不斷被加深認識的。產品經理的說辭是經常變化的。當你面向需求程式設計,你就是在依賴一個認識每一秒都在改變的女/男朋友。你將身心俱疲。

我們應該面向業務模型程式設計。我在《Code Review 我都 CR 些什麼》裡也提到了這一點,但是我當時並沒有給出應該怎麼去設計業務模型的指導。我的潛臺詞就是,你還是僅僅能憑藉自己的智力和經驗,沒有很多方法論工具。

現在,我給你推薦一個工具,DDD(Domain-Driven Design),面向領域驅動設計。它能讓你對業務更好地建模,讓對業務建模變成一個可拆解的執行步驟,僅僅需要少得多的智力和經驗。區分好領域上下文,思考明白它們之間的關係,找到領域下的實體和值物件,找到和模型貼合的架構方案。這些任務,讓業務建模變得簡單。

當我們面向業務模型程式設計,變更的需求就變成了--提供給使用者他所需要的業務模型的不同部分。我們不再是在不斷地 change 程式碼,而是在不斷地 extend 程式碼,逐漸做完一個業務模型的填空題。

寫程式碼要有對於'美'的追求

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

很無奈,我把對美的追求說得這麼'卑微'。必須要由'務實的需要'來構建必要性。而不是每個工程師發自內心的,像對待漂亮的異性、好的音樂、好的電影一樣的發自內心的需要它。認為程式碼也是取悅別人、取悅自己的東西。

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

萬字詳文闡釋程式設計師修煉之道
萬字詳文闡釋程式設計師修煉之道
萬字詳文闡釋程式設計師修煉之道

應用程式框架是實現細節

以下是《整潔架構之道》的原文摘抄:

萬字詳文闡釋程式設計師修煉之道

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

  • 核心程式碼應該透過 DIP 來讓它不要和具體框架繫結!它應該使用 DIP(比如代理類),抽象出一個防腐層,讓自己的核心程式碼免於腐壞。
  • 選擇一個框架,你不去做防腐層(主要透過 DIP),你就是單方面領了結婚證,你只有義務,沒有權利。同學們要想明白。同學們應該對框架本身是否優秀,是否足夠元件化,它本身能否在專案裡做到可插拔,做出思考和設計。

trpc-go 對於外掛化這事兒,做得還不錯,大家會積極地使用它。trpc-cpp 離外掛化非常遠,它自己根本就成不了一個外掛,而是有一種要強暴你的感覺,你能憑直覺明顯地感覺到不願意和它訂終身。例如,trpc-cpp 甚至強暴了你構建、編譯專案的方式。當然,這很多時候是 c++語言本身的問題。

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

一切都應該是程式碼(透過程式碼去顯式組合)

Unix 程式設計哲學告訴我們: 如果有一些引數是可變的,我們應該使用配置,而不是把引數寫死在程式碼裡。在騰訊,這一點做得很好。但是,大人,現在時代又變了。

J2EE 框架讓我們看到,元件也可以是透過配置 Java Bean 的形式注入到框架裡的。J2EE 實現了把元件也配置化的壯舉。但是,時代變了!你下載一個 golang 編譯器,你進入你下載的檔案裡去看,會發現你找不到任何配置檔案。這是為什麼?兩個簡單,但是很多年都被人們忽略的道理:

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

golang 的編譯器是怎麼做的呢?它會在程式碼裡給你設定一個通用性較強的預設配置項。同時,配置項都是集中管理的,就像管理配置檔案一樣。你可以透過額外配置一個配置檔案或者命令列引數,來改變編譯器的行為。這就變成了,程式碼解釋了每一個配置項是用來做什麼的。只有當你需要的時候,你會先看懂程式碼,然後,當你有需求的時候,透過額外的配置去改變一個你有預期的行為。

邏輯變成了。一開始,所有事情都是解耦的。一件事情都只看一塊程式碼就能明白。程式碼有較好的自解釋性和註解,不再需要費勁地編寫撇腳的文件。當你明白之後,你需要不一樣的行為,就透過額外的配置來實現。關於怎麼配置,程式碼裡也講明白了。

對於 trpc-go 框架,以及一眾外掛,優先考慮配置,然後才是程式碼去指定,部分功能還只能透過配置去指定,我就很難受。我接受它,就得把一個事情放在兩個地方去完成:

  • 需要在程式碼裡 import 外掛包。
  • 需要在配置檔案裡配置外掛引數。

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

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

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

  • 按層封裝
  • 按功能封裝
  • 按領域封裝
  • 按元件封裝

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

所以,所有細節都是關鍵的。每一塊磚頭都被精心設計,才能構建一個漂亮的專案!

所有細節都應該被顯式處理

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

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

萬字詳文闡釋程式設計師修煉之道

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

合理註釋一些並不'通俗'的邏輯和數值

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

萬字詳文闡釋程式設計師修煉之道

習慣留下 TODO

要這麼做的道理很簡單。便於所有人能接著你開發。極有可能就是你自己接著自己開發。如果沒有標註 TODO 把沒有做完的事情標示出來。可能,你自己都會搞忘自己有事兒沒做完了。留下 TODO 是很簡單的事情,我們為什麼不做呢?

萬字詳文闡釋程式設計師修煉之道

不要丟棄錯誤資訊

即'錯誤傳遞原則'。這裡給它換個名字--你不應該主動把很多有用的資訊給丟棄了。

萬字詳文闡釋程式設計師修煉之道

自動化測試要快

在 google,自動化測試是硬性要求在限定時間內跑完的。這從細節上保障了自動化測試的速度,進而保障了自動化測試的價值和可用性。你真的需要 sleep 這麼久?應該認真考量。考量清楚了把原因寫下來。當大家發現總時長太長的時候,可以選擇其中最不必要的部分做最佳化。

萬字詳文闡釋程式設計師修煉之道

歷史有問題的程式碼, 發現了問題要及時 push 相關人主動解決

這是'控制軟體的熵是軟體工程的重要任務之一'的表現之一。我們是團隊作戰,不是無組織無記錄的部隊。發現了問題,就及時丟擲和解決。讓傷痛更少,跑得更快。

萬字詳文闡釋程式設計師修煉之道

less is more

less is more. 《Code Review 我都 CR 些什麼》強調過了,這裡不再強調。

萬字詳文闡釋程式設計師修煉之道

如果打了錯誤日誌, 有效資訊必須充足, 且不過多

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

萬字詳文闡釋程式設計師修煉之道

註釋要把問題講清楚, 講不清楚的日誌等於沒有

是個簡單的道理,和'所有細節都應該被顯式處理'一脈相承。

萬字詳文闡釋程式設計師修煉之道

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

你也會成為 reviewer,節省他人的時間,他人也節省你的時間。縮短互動次數,提升 review 的愉悅感。讓他人提的 comment 都是'言之有物'的東西,而不是一些反反覆覆的最基礎的細節。會讓他人更愉悅,自己在看 comment 的時候,也更愉悅,更願意去討論、溝通。讓 code review 成為一個技術交流的平臺。

萬字詳文闡釋程式設計師修煉之道

要尋找合適的定語

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

萬字詳文闡釋程式設計師修煉之道

不要出現特定 IP,或者把什麼可變的東西寫死

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

萬字詳文闡釋程式設計師修煉之道

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

這個存粹就是放縱自己了。當然,也會有隻能用 1、2、3、4 的時候。但是,你這裡,是麼?多數時候,都不會是。

萬字詳文闡釋程式設計師修煉之道

有必要才使用 init

這,也顯而易見。init 很方便,但是,它也會帶來心智負擔。

萬字詳文闡釋程式設計師修煉之道

要關注 shadow write

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

萬字詳文闡釋程式設計師修煉之道

能不耦合接收器就別耦合

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

萬字詳文闡釋程式設計師修煉之道

空實現需要註明空實現就是實現

這個和'所有細節都應該被顯式處理'一脈相承。這個理念,我見過無數種形式表現出來。這裡就是其中一種。列舉這個 case,讓你印象再深刻一點。

萬字詳文闡釋程式設計師修煉之道

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

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

展望

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

寫到最後,我發現,我上面寫的這些東西都不那麼重要了。你有想把程式碼寫得更利於團隊協作的價值觀和態度,反而是最重要的事情。上面講的都僅僅是寫高質量程式碼的手段和思想方法。當你認可了'應該編寫利於團隊協作的高質量程式碼',並且擁有對'不利於團隊程式碼質量的程式碼'嫉惡如仇的態度。你總能找到高質量程式碼的寫法。沒有我幫你總結,你也總會掌握!

拾遺

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


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559354/viewspace-2740217/,如需轉載,請註明出處,否則將追究法律責任。

相關文章