騰訊 Code Review 規(guī)范出爐!你還敢亂寫代碼??
????點(diǎn)擊上方“程序IT圈”,選擇“設(shè)為星標(biāo)”
? ?每天下午更新文章,每天成長(zhǎng)一點(diǎn)點(diǎn)
來源:騰訊技術(shù)工程?
cheaterlin
周末愉快,看到一篇不錯(cuò)的 Code Review 的文章,胖友們可以學(xué)習(xí)學(xué)習(xí)。
避免大型鞭尸現(xiàn)場(chǎng)~
前言 為什么技術(shù)人員包括 leader 都要做 code review 為什么同學(xué)們要在 review 中思考和總結(jié)最佳實(shí)踐 代碼變壞的根源 重復(fù)的代碼 早期有效的決策不再有效 過早的優(yōu)化 對(duì)合理性沒有苛求 總是面向?qū)ο?總喜歡封裝 根本沒有設(shè)計(jì) 必須形而上的思考 model 設(shè)計(jì) UNIX 設(shè)計(jì)哲學(xué) 具體實(shí)踐點(diǎn) 主干開發(fā) 《unix 編程藝術(shù)》
前言
作為公司代碼委員會(huì) golang 分會(huì)的理事,我 review 了很多代碼,看了很多別人的 review 評(píng)論。發(fā)現(xiàn)不少同學(xué) code review 與寫出好代碼的水平有待提高。在這里,想分享一下我的一些理念和思路。
為什么技術(shù)人員包括 leader 都要做 code review
諺語(yǔ)曰: 'Talk Is Cheap, Show Me The Code'。知易行難,知行合一難。嘴里要講出來總是輕松,把別人講過的話記住,組織一下語(yǔ)言,再講出來,很容易。絕知此事要躬行。設(shè)計(jì)理念你可能道聽途說了一些,以為自己掌握了,但是你會(huì)做么?有能力去思考、改進(jìn)自己當(dāng)前的實(shí)踐方式和實(shí)踐中的代碼細(xì)節(jié)么?不客氣地說,很多人僅僅是知道并且認(rèn)同了某個(gè)設(shè)計(jì)理念,進(jìn)而產(chǎn)生了一種虛假的安心感---自己的技術(shù)并不差。但是,他根本沒有去實(shí)踐這些設(shè)計(jì)理念,甚至根本實(shí)踐不了這些設(shè)計(jì)理念,從結(jié)果來說,他懂不懂這些道理/理念,有什么差別?變成了自欺欺人。
代碼,是設(shè)計(jì)理念落地的地方,是技術(shù)的呈現(xiàn)和根本。同學(xué)們可以在 review 過程中做到落地溝通,不再是空對(duì)空的討論,可以在實(shí)際問題中產(chǎn)生思考的碰撞,互相學(xué)習(xí),大家都掌握?qǐng)F(tuán)隊(duì)里積累出來最好的實(shí)踐方式!當(dāng)然,如果 leader 沒時(shí)間寫代碼,僅僅是 review 代碼,指出其他同學(xué)某些實(shí)踐方式不好,要給出好的實(shí)踐的意見,即使沒親手寫代碼,也是對(duì)最佳實(shí)踐要有很多思考。
為什么同學(xué)們要在 review 中思考和總結(jié)最佳實(shí)踐
我這里先給一個(gè)我自己的總結(jié):所謂架構(gòu)師,就是掌握大量設(shè)計(jì)理念和原則、落地到各種語(yǔ)言及附帶工具鏈(生態(tài))下的實(shí)踐方法、垂直行業(yè)模型理解,定制系統(tǒng)模型設(shè)計(jì)和工程實(shí)踐規(guī)范細(xì)則。進(jìn)而控制 30+萬(wàn)行代碼項(xiàng)目的開發(fā)便利性、可維護(hù)性、可測(cè)試性、運(yùn)營(yíng)質(zhì)量。
厲害的技術(shù)人,主要可以分為下面幾個(gè)方向:
奇技淫巧
掌握很多技巧,以及發(fā)現(xiàn)技巧一系列思路,比如很多編程大賽,比的就是這個(gè)。但是,這個(gè)對(duì)工程,用處好像并不是很大。
領(lǐng)域奠基
比如約翰*卡馬克,他創(chuàng)造出了現(xiàn)代計(jì)算機(jī)圖形高效渲染的方法論。不論如果沒有他,后面會(huì)不會(huì)有人發(fā)明,他就是第一個(gè)發(fā)明了。1999 年,卡馬克登上了美國(guó)時(shí)代雜志評(píng)選出來的科技領(lǐng)域 50 大影響力人物榜單,并且名列第 10 位。但是,類似的殿堂級(jí)位置,沒有幾個(gè),不夠大家分,沒我們的事兒。
理論研究
八十年代李開復(fù)博士堅(jiān)持采用隱含馬爾可夫模型的框架,成功地開發(fā)了世界上第一個(gè)大詞匯量連續(xù)語(yǔ)音識(shí)別系統(tǒng) Sphinx。我輩工程師,好像擅長(zhǎng)這個(gè)的很少。
產(chǎn)品成功
小龍哥是標(biāo)桿。
最佳實(shí)踐
這個(gè)是大家都可以做到,按照上面架構(gòu)師的定義。在這條路上走得好,就能為任何公司組建技術(shù)團(tuán)隊(duì),組織建設(shè)高質(zhì)量的系統(tǒng)。
從上面的討論中,可以看出,我們普通工程師的進(jìn)化之路,就是不斷打磨最佳實(shí)踐方法論、落地細(xì)節(jié)。
代碼變壞的根源
在討論什么代碼是好代碼之前,我們先討論什么是不好的。計(jì)算機(jī)是人造的學(xué)科,我們自己制造了很多問題,進(jìn)而去思考解法。
重復(fù)的代碼
//?BatchGetQQTinyWithAdmin?獲取QQ?uin的tinyID,?需要主uin的tiny和登錄態(tài)
//?friendUins?可以是空列表,?只要admin?uin的tiny
func?BatchGetQQTinyWithAdmin(ctx?context.Context,?adminUin?uint64,?friendUin?[]uint64)?(
?adminTiny?uint64,?sig?[]byte,?frdTiny?map[uint64]uint64,?err?error)?{
?var?friendAccountList?[]*basedef.AccountInfo
?for?_,?v?:=?range?friendUin?{
??friendAccountList?=?append(friendAccountList,?&basedef.AccountInfo{
???AccountType:?proto.String(def.StrQQU),
???Userid:?proto.String(fmt.Sprint(v)),
??})
?}
?req?:=?&cmd0xb91.ReqBody{
??Appid:?proto.Uint32(model.DocAppID),
??CheckMethod:?proto.String(CheckQQ),
??AdminAccount:?&basedef.AccountInfo{
???AccountType:?proto.String(def.StrQQU),
???Userid:?proto.String(fmt.Sprint(adminUin)),
??},
??FriendAccountList:?friendAccountList,
?}
因?yàn)樽铋_始協(xié)議設(shè)計(jì)得不好,第一個(gè)使用接口的人,沒有類似上面這個(gè)函數(shù)的代碼,自己實(shí)現(xiàn)了一個(gè)嵌入邏輯代碼的填寫請(qǐng)求結(jié)構(gòu)結(jié)構(gòu)體的代碼,一開始,挺好的。但當(dāng)有第二個(gè)人,第三個(gè)人干了類似的事情,我們將無法再重構(gòu)這個(gè)協(xié)議,必須做到麻煩的向前兼容。而且每個(gè)同學(xué),都要理解一遍上面這個(gè)協(xié)議怎么填,理解有問題,就觸發(fā) bug。或者,如果某個(gè)錯(cuò)誤的理解,普遍存在,我們就得找到所有這些重復(fù)的片段,都修改一遍。
當(dāng)你要讀一個(gè)數(shù)據(jù),發(fā)現(xiàn)兩個(gè)地方有,不知道該選擇哪個(gè)。當(dāng)你要實(shí)現(xiàn)一個(gè)功能,發(fā)現(xiàn)兩個(gè) rpc 接口、兩個(gè)函數(shù)能做到,你不知道選哪一個(gè)。你有面臨過這樣的'人生難題'么?其實(shí)怎么選并不重要了,你寫的這個(gè)代碼已經(jīng)在走向 shit 的道路上邁出了堅(jiān)實(shí)的一步。
但是,A little copying is better than a little dependency。這里提一嘴,不展開。
這里,我必須額外說一句。大家使用 trpc。感覺自己被鼓勵(lì)'每個(gè)服務(wù)搞一個(gè) git'。那,你這個(gè)服務(wù)里訪問 db 的代碼,rpc 的代碼,各種可以復(fù)用的代碼,是用的大家都復(fù)用的 git 下的代碼么?每次都重復(fù)寫一遍,db 字段細(xì)節(jié)改了,每個(gè)使用過 db 的 server 對(duì)應(yīng)的 git 都改一遍?這個(gè)通用 git 已經(jīng)寫好的接口應(yīng)該不知道哪些 git 下的代碼因?yàn)樽约翰幌蚯凹嫒莸男薷亩肋h(yuǎn)放棄了向前不兼容的修改?
早期有效的決策不再有效
很多時(shí)候,我們第一版代碼寫出來,是沒有太大的問題的。比如,下面這個(gè)代碼
//?Update?增量更新
func?(s?*FilePrivilegeStore)?Update(key?def.PrivilegeKey,
?clear,?isMerge?bool,?subtract?[]*access.AccessInfo,?increment?[]*access.AccessInfo,
?policy?*uint32,?adv?*access.AdvPolicy,?shareKey?string,?importQQGroupID?uint64)?error?{
?//?獲取之前的數(shù)據(jù)
?info,?err?:=?s.Get(key)
?if?err?!=?nil?{
??return?err
?}
?incOnlyModify?:=?update(info,?&key,?clear,?subtract,
??increment,?policy,?adv,?shareKey,?importQQGroupID)
?stat?:=?statAndUpdateAccessInfo(info)
?if?!incOnlyModify?{
??if?stat.groupNumber?>?model.FilePrivilegeGroupMax?{
???return?errors.Errorf(errors.PrivilegeGroupLimit,
????"group?num?%d?larger?than?limit?%d",
????stat.groupNumber,?model.FilePrivilegeGroupMax)
??}
?}
?if?!isMerge?{
??if?key.DomainID?==?uint64(access.SPECIAL_FOLDER_DOMAIN_ID)?&&
???len(info.AccessInfos)?>?model.FilePrivilegeMaxFolderNum?{
???return?errors.Errorf(errors.PrivilegeFolderLimit,
????"folder?owner?num?%d?larger?than?limit?%d",
????len(info.AccessInfos),?model.FilePrivilegeMaxFolderNum)
??}
??if?len(info.AccessInfos)?>?model.FilePrivilegeMaxNum?{
???return?errors.Errorf(errors.PrivilegeUserLimit,
????"file?owner?num?%d?larger?than?limit?%d",
????len(info.AccessInfos),?model.FilePrivilegeMaxNum)
??}
?}
?pbDataSt?:=?infoToData(info,?&key)
?var?updateBuf?[]byte
?if?updateBuf,?err?=?proto.Marshal(pbDataSt);?err?!=?nil?{
??return?errors.Wrapf(err,?errors.MarshalPBError,
???"FilePrivilegeStore.Update?Marshal?data?error,?key[%v]",?key)
?}
?if?err?=?s.setCKV(generateKey(&key),?updateBuf);?err?!=?nil?{
??return?errors.Wrapf(err,?errors.Code(err),
???"FilePrivilegeStore.Update?setCKV?error,?key[%v]",?key)
?}
?return?nil
}
現(xiàn)在看,這個(gè)代碼挺好的,長(zhǎng)度沒超過 80 行,邏輯比價(jià)清晰。但是當(dāng) isMerge 這里判斷邏輯,如果加入更多的邏輯,把局部行數(shù)撐到 50 行以上,這個(gè)函數(shù),味道就壞了。出現(xiàn)兩個(gè)問題:
1)函數(shù)內(nèi)代碼不在一個(gè)邏輯層次上,閱讀代碼,本來在閱讀著頂層邏輯,突然就掉入了長(zhǎng)達(dá) 50 行的 isMerge 的邏輯處理細(xì)節(jié),還沒看完,讀者已經(jīng)忘了前面的代碼講了什么,需要來回看,挑戰(zhàn)自己大腦的 cache 尺寸。
2)代碼有問題后,再新加代碼的同學(xué),是改還是不改前人寫好的代碼呢?出 bug 誰(shuí)來背?這是一個(gè)靈魂拷問。
過早的優(yōu)化
這個(gè)大家聽了很多了,這里不贅述。
對(duì)合理性沒有苛求
'兩種寫法都 ok,你隨便挑一種吧','我這樣也沒什么吧',這是我經(jīng)常聽到的話。
//?Get?獲取IP
func?(i?*IPGetter)?Get(cardName?string)?string?{
?i.l.RLock()
?ip,?found?:=?i.m[cardName]
?i.l.RUnlock()
?if?found?{
??return?ip
?}
?i.l.Lock()
?var?err?error
?ip,?err?=?getNetIP(cardName)
?if?err?==?nil?{
??i.m[cardName]?=?ip
?}
??i.l.Unlock()
?return?ip
}
i.l.Unlock()可以放在當(dāng)前的位置,也可以放在 i.l.Lock()下面,做成 defer。兩種在最初構(gòu)造的時(shí)候,好像都行。這個(gè)時(shí)候,很多同學(xué)態(tài)度就變得不堅(jiān)決。實(shí)際上,這里必須是 defer 的。
??i.l.Lock()
?defer?i.l.Unlock()
?var?err?error
?ip,?err?=?getNetIP(cardName)
?if?err?!=?nil?{
??return?"127.0.0.1"
?}
?i.m[cardName]?=?ip
?return?ip
這樣的修改,是極有可能發(fā)生的,它還是要變成 defer,那,為什么不一開始就是 defer,進(jìn)入最合理的狀態(tài)?不一開始就進(jìn)入最合理的狀態(tài),在后續(xù)協(xié)作中,其他同學(xué)很可能犯錯(cuò)!
總是面向?qū)ο?總喜歡封裝
我是軟件工程科班出身。學(xué)的第一門編程語(yǔ)言是 c++。教材是這本 。當(dāng)時(shí)自己讀完教材,初入程序設(shè)計(jì)之門,對(duì)于里面講的'封裝',驚為天人,多么美妙的設(shè)計(jì)啊,面向?qū)ο?,多么智慧的設(shè)計(jì)啊。但是,這些年來,我看到了大牛'云風(fēng)'對(duì)于'畢業(yè)生使用 mysql api 就喜歡搞個(gè) class 封裝再用'的嘲諷;看到了各種莫名其妙的 class 定義;體會(huì)到了經(jīng)常要去看一個(gè)莫名其妙的繼承樹,必須要把整個(gè)繼承樹整體讀明白才能確認(rèn)一個(gè)細(xì)小的邏輯分支;多次體會(huì)到了我需要辛苦地壓抑住自己的抵觸情緒,去細(xì)度一個(gè)自作聰明的被封裝的代碼,確認(rèn)我的 bug。除了 UI 類場(chǎng)景,我認(rèn)為少用繼承、多用組合。
template<class?_PKG_TYPE>
class?CSuperAction?:?public?CSuperActionBase?{
??public:
????typedef?_PKG_TYPE?pkg_type;
????typedef?CSuperAction?this_type;
????...
}
這是 sspp 的代碼。CSuperAction 和 CSuperActionBase,一會(huì)兒 super,一會(huì)兒 base,Super 和 SuperBase 是在怎樣的兩個(gè)抽象層次上,不通讀代碼,沒人能讀明白。我想確認(rèn)任何細(xì)節(jié),都要把多個(gè)層次的代碼都通讀了,有什么封裝性可言?
好,你說是作者沒有把 class name 取得好。那,問題是,你能取得好么?一個(gè)剛?cè)肼毜?T1.2 的同學(xué)能把 class name、class 樹設(shè)計(jì)得好么?即使是對(duì)簡(jiǎn)單的業(yè)務(wù)模型,也需要無數(shù)次'壞'的對(duì)象抽象實(shí)踐,才能培養(yǎng)出一個(gè)具有合格的 class 抽象能力的同學(xué),這對(duì)于大型卻松散的團(tuán)隊(duì)協(xié)作,不是破壞性的?已經(jīng)有了一套繼承樹,想要添加功能就只能在這個(gè)繼承樹里添加,以前的繼承樹不再適合新的需求,這個(gè)繼承樹上所有的 class,以及使用它們的地方,你都去改?不,是個(gè)正常人都會(huì)放棄,開始堆屎山。
封裝,就是我可以不關(guān)心實(shí)現(xiàn)。但是,做一個(gè)穩(wěn)定的系統(tǒng),每一層設(shè)計(jì)都可能出問題。abi,總有合適的用法和不合適的用法,真的存在我們能完全不關(guān)心封裝的部分是怎么實(shí)現(xiàn)的?不,你不能。bug 和性能問題,常常就出現(xiàn)在,你用了錯(cuò)誤的用法去使用一個(gè)封裝好的函數(shù)。即使是 android、ios 的 api,golang、java 現(xiàn)成的 api,我們常常都要去探究實(shí)現(xiàn),才能把 api 用好。
那,我們是不是該一上來,就做一個(gè)透明性很強(qiáng)的函數(shù),才更為合理?使用者想知道細(xì)節(jié),進(jìn)來吧,我的實(shí)現(xiàn)很易讀,你看看就明白,使用時(shí)不會(huì)迷路!對(duì)于邏輯復(fù)雜的函數(shù),我們還要強(qiáng)調(diào)函數(shù)內(nèi)部工作方式'可以讓讀者在大腦里想象呈現(xiàn)完整過程'的可現(xiàn)性,讓使用者輕松讀懂,有把握,使用時(shí),不迷路!
根本沒有設(shè)計(jì)
這個(gè)最可怕,所有需求,上手就是一頓擼,'設(shè)計(jì)是什么東西?我一個(gè)文件 5w 行,一個(gè)函數(shù) 5k 行,干不完需求?'從第一行代碼開始,就是無設(shè)計(jì)的,隨意地踩著滿地的泥坑,對(duì)于旁人的眼光沒有感覺,一個(gè)人獨(dú)舞,產(chǎn)出的代碼,完成了需求,毀滅了接手自己代碼的人。這個(gè)就不舉例了,每個(gè)同學(xué)應(yīng)該都能在自己的項(xiàng)目類發(fā)現(xiàn)這種代碼。關(guān)注公眾號(hào):Java后端編程,學(xué)習(xí)更多Java知識(shí)。
必須形而上的思考
常常,同學(xué)們聽演講,公開課,就喜歡聽一些細(xì)枝末節(jié)的'干活'。這沒有問題。但是,你干了幾年活,學(xué)習(xí)了多少干貨知識(shí)點(diǎn)?構(gòu)建起自己的技術(shù)思考'面',進(jìn)入立體的'工程思維',把技術(shù)細(xì)節(jié)和系統(tǒng)要滿足的需求在思考上連接起來了么?當(dāng)聽一個(gè)需求的時(shí)候,你能思考到自己的 code package 該怎么組織,函數(shù)該怎么組織了么?
那,技術(shù)點(diǎn)要怎么和需求連接起來呢?答案很簡(jiǎn)單,你需要在時(shí)間里總結(jié),總結(jié)出一些明確的原則、思維過程。思考怎么去總結(jié),特別像是在思考哲學(xué)問題。從一些瑣碎的細(xì)節(jié)中,由具體情況上升到一些原則、公理。同時(shí),大家在接受原則時(shí),不應(yīng)該是接受和記住原則本身,而應(yīng)該是結(jié)構(gòu)原則,讓這個(gè)原則在自己這里重新推理一遍,自己完全掌握這個(gè)原則的適用范圍。
再進(jìn)一步具體地說,對(duì)于工程最佳實(shí)踐的形而上的思考過程,就是:
把工程實(shí)踐中遇到的問題,從問題類型和解法類型,兩個(gè)角度去歸類,總結(jié)出一些有限適用的原則,就從點(diǎn)到了面。把諸多總結(jié)出的原則,組合應(yīng)用到自己的項(xiàng)目代碼中,就是把多個(gè)面結(jié)合起來構(gòu)建了一套立體的最佳實(shí)踐的方案。當(dāng)你這套方案能適應(yīng) 30w+行代碼的項(xiàng)目,超過 30 人的項(xiàng)目,你就架構(gòu)師入門了!當(dāng)你這個(gè)項(xiàng)目,是多端,多語(yǔ)言,代碼量超過 300w 行,參與人數(shù)超過 300 人,代碼質(zhì)量依然很高,代碼依然在高效地自我迭代,每天消除掉過時(shí)的代碼,填充高質(zhì)量的替換舊代碼和新生的代碼。
恭喜你,你已經(jīng)是一個(gè)很高級(jí)的架構(gòu)師了!再進(jìn)一步,你對(duì)某個(gè)業(yè)務(wù)模型有獨(dú)到或者全面的理解,構(gòu)建了一套行業(yè)第一的解決方案,結(jié)合剛才高質(zhì)量實(shí)現(xiàn)的能力,實(shí)現(xiàn)了這么一個(gè)項(xiàng)目。沒啥好說的,你已經(jīng)是專家工程師了。級(jí)別再高,我就不了解了,不在這里討論。
那么,我們要重頭開始積累思考和總結(jié)?不,有一本書叫做《unix 編程藝術(shù)》,我在不同的時(shí)期分別讀了 3 遍,等一會(huì),我講一些里面提到的,我覺得在騰訊尤其值得拿出來說的原則。這些原則,正好就能作為 code review 時(shí)大家判定代碼質(zhì)量的準(zhǔn)繩。但,在那之前,我得講一下另外一個(gè)很重要的話題,模型設(shè)計(jì)。
model 設(shè)計(jì)
沒讀過 oauth2.0 RFC,就去設(shè)計(jì)第三方授權(quán)登陸的人,終歸還要再發(fā)明一個(gè)撇腳的 oauth。
2012 年我剛畢業(yè),我和一個(gè)去了廣州聯(lián)通公司的華南理工畢業(yè)生聊天。當(dāng)時(shí)他說他工作很不開心,因?yàn)楣ぷ骼锊唤?jīng)常寫代碼,而且認(rèn)為自己有 ACM 競(jìng)賽金牌級(jí)的算法熟練度+對(duì) CPP 代碼的熟悉,寫下一個(gè)個(gè)指針操作內(nèi)存,什么程序?qū)懖怀鰜?,什么事情做不好。?dāng)時(shí)我覺得,挺有道理,編程工具在手,我什么事情做不了?
現(xiàn)在,我會(huì)告訴他,復(fù)雜如 linux 操作系統(tǒng)、Chromium 引擎、windows office,你做不了。原因是,他根本沒進(jìn)入軟件工程的工程世界。不是會(huì)搬磚就能修出港珠澳大橋。但是,這么回答并不好,舉證用的論據(jù)離我們太遙遠(yuǎn)了。見微知著。我現(xiàn)在會(huì)回答,你做不了,簡(jiǎn)單如一個(gè)權(quán)限系統(tǒng),你知道怎么做么?堆積一堆邏輯層次一維展開的 if else?簡(jiǎn)單如一個(gè)共享文件管理,你知道怎么做么?堆積一堆邏輯層次一維展開的 ife lse?你聯(lián)通有上萬(wàn)臺(tái)服務(wù)器,你要怎么寫一個(gè)管理平臺(tái)?堆積一堆邏輯層次一維展開的 ife lse?
上來就是干,能實(shí)現(xiàn)上面提到的三個(gè)看似簡(jiǎn)單的需求?想一想,亞馬遜、阿里云折騰了多少年,最后才找到了容器+Kubernetes 的大殺器。這里,需要谷歌多少年在 BORG 系統(tǒng)上的實(shí)踐,提出了優(yōu)秀的服務(wù)編排領(lǐng)域模型。權(quán)限領(lǐng)域,有 RBAC、DAC、MAC 等等模型,到了業(yè)務(wù),又會(huì)有細(xì)節(jié)的不同。如 Domain Driven Design 說的,沒有良好的領(lǐng)域思考和模型抽象,邏輯復(fù)雜度就是 n^2 指數(shù)級(jí)的,你得寫多少 ifelse,得思考多少可能的 if 路徑,來 cover 所有的不合符預(yù)期的情況。你必須要有 Domain 思考探索、model 拆解/抽象/構(gòu)建的能力。
有人問過我,要怎么有效地獲得這個(gè)能力?這個(gè)問題我沒能回答,就像是在問我,怎么才能獲得 MIT 博士的學(xué)術(shù)能力?我無法回答。唯一回答就是,進(jìn)入某個(gè)領(lǐng)域,就是首先去看前人的思考,站在前人的肩膀上,再用上自己的通識(shí)能力,去進(jìn)一步思考。至于怎么建立好的通識(shí)思考能力,可能得去常青藤讀個(gè)書吧:)或者,就在工程實(shí)踐中思考和鍛煉自己的這個(gè)能力!
同時(shí),基于 model 設(shè)計(jì)的代碼,能更好地適應(yīng)產(chǎn)品經(jīng)理不斷變更的需求。比如說,一個(gè) calendar(日歷)應(yīng)用,簡(jiǎn)單來想,不要太簡(jiǎn)單!以'userid_date'為 key 記錄一個(gè)用戶的每日安排不就完成了么?只往前走一步,設(shè)計(jì)了一個(gè)任務(wù),上限分發(fā)給 100w 個(gè)人,創(chuàng)建這么一個(gè)任務(wù),是往 100w 個(gè)人下面添加一條記錄?你得改掉之前的設(shè)計(jì),換 db。再往前走一步,要拉出某個(gè)用戶和某個(gè)人一起要參與的所有事務(wù),是把兩個(gè)人的所有任務(wù)來做 join?好像還行。如果是和 100 個(gè)人一起參與的所有任務(wù)呢?100 個(gè)人的任務(wù)來 join?不現(xiàn)實(shí)了吧。
好,你引入一個(gè)群組 id,那么,你最開始的'userid_date'為 key 的設(shè)計(jì),是不是又要修改和做數(shù)據(jù)遷移了?經(jīng)常來一個(gè)需求,你就得把系統(tǒng)推翻重來,或者根本就只能拒絕用戶的需求,這樣的戰(zhàn)斗力,還好意思叫自己工程師?你一開始就應(yīng)該思考自己面對(duì)的業(yè)務(wù)領(lǐng)域,思考自己的日歷應(yīng)用可能的模型邊界,把可能要做的能力都拿進(jìn)來思考,構(gòu)建一個(gè) model,設(shè)計(jì)一套通用的 store 層接口,基于通用接口的邏輯代碼。當(dāng)產(chǎn)品不斷發(fā)展,就是不停往模型里填內(nèi)容,而不是推翻重來。
這,思考模型邊界,構(gòu)建模型細(xì)節(jié),就是兩個(gè)很重要的能力,也是絕大多數(shù)騰訊產(chǎn)品經(jīng)理不具備的能力,你得具備,對(duì)整個(gè)團(tuán)隊(duì)都是極其有益的。你面對(duì)產(chǎn)品經(jīng)理時(shí),就聽取他們出于對(duì)用戶體驗(yàn)負(fù)責(zé)思考出的需求點(diǎn),到你自己這里,用一個(gè)完整的模型去涵蓋這些零碎的點(diǎn)。
model 設(shè)計(jì),是形而上思考中的一個(gè)方面,一個(gè)特別重要的方面。接下來,我們來抄襲抄襲 unix 操作系統(tǒng)構(gòu)建的實(shí)踐為我們提出的前人實(shí)踐經(jīng)驗(yàn)和'公理'總結(jié)。在自己的 coding/code review 中,站在巨人的肩膀上去思考。不重復(fù)地發(fā)現(xiàn)經(jīng)典力學(xué),而是往相對(duì)論挺進(jìn)。
UNIX 設(shè)計(jì)哲學(xué)
不懂 Unix 的人注定最終還要重復(fù)發(fā)明一個(gè)撇腳的 Unix。--Henry Spenncer, 1987.11
下面這一段話太經(jīng)典,我必須要摘抄一遍(自《UNIX 編程藝術(shù)》):“工程和設(shè)計(jì)的每個(gè)分支都有自己的技術(shù)文化。在大多數(shù)工程領(lǐng)域中,就一個(gè)專業(yè)人員的素養(yǎng)組成來說,有些不成文的行業(yè)素養(yǎng)具有與標(biāo)準(zhǔn)手冊(cè)及教科書同等重要的地位(并且隨著專業(yè)人員經(jīng)驗(yàn)的日積月累,這些經(jīng)驗(yàn)常常會(huì)比書本更重要)。資深工程師們?cè)诠ぷ髦袝?huì)積累大量的隱性知識(shí),他們用類似禪宗'教外別傳'的方式,通過言傳身教傳授給后輩。軟件工程算是此規(guī)則的一個(gè)例外:技術(shù)變革如此之快,軟件環(huán)境日新月異,軟件技術(shù)文化暫如朝露。
然而,例外之中也有例外。確有極少數(shù)軟件技術(shù)被證明經(jīng)久耐用,足以演進(jìn)為強(qiáng)勢(shì)的技術(shù)文化、有鮮明特色的藝術(shù)和世代相傳的設(shè)計(jì)哲學(xué)?!?/p>
接下來,我用我的理解,講解一下幾個(gè)我們常常做不到的原則。
Keep It Simple Stuped!
KISS 原則,大家應(yīng)該是如雷貫耳了。但是,你真的在遵守?什么是 Simple?簡(jiǎn)單?golang 語(yǔ)言主要設(shè)計(jì)者之一的 Rob Pike 說'大道至簡(jiǎn)',這個(gè)'簡(jiǎn)'和簡(jiǎn)單是一個(gè)意思么?
首先,簡(jiǎn)單不是面對(duì)一個(gè)問題,我們印入眼簾第一映像的解法為簡(jiǎn)單。我說一句,感受一下。"把一個(gè)事情做出來容易,把事情用最簡(jiǎn)單有效的方法做出來,是一個(gè)很難的事情。"比如,做一個(gè)三方授權(quán),oauth2.0 很簡(jiǎn)單,所有概念和細(xì)節(jié)都是緊湊、完備、易用的。
你覺得要設(shè)計(jì)到 oauth2.0 這個(gè)效果很容易么?要做到簡(jiǎn)單,就要對(duì)自己處理的問題有全面的了解,然后需要不斷積累思考,才能做到從各個(gè)角度和層級(jí)去認(rèn)識(shí)這個(gè)問題,打磨出一個(gè)通俗、緊湊、完備的設(shè)計(jì),就像 ios 的交互設(shè)計(jì)。簡(jiǎn)單不是容易做到的,需要大家在不斷的時(shí)間和 code review 過程中去積累思考,pk 中觸發(fā)思考,交流中總結(jié)思考,才能做得愈發(fā)地好,接近'大道至簡(jiǎn)'。
兩張經(jīng)典的模型圖,簡(jiǎn)單又全面,感受一下,沒看懂,可以立即自行 google 學(xué)習(xí)一下:RBAC:
logging:

原則 3 組合原則: 設(shè)計(jì)時(shí)考慮拼接組合
關(guān)于 OOP,關(guān)于繼承,我前面已經(jīng)說過了。那我們?cè)趺唇M織自己的模塊?對(duì),用組合的方式來達(dá)到。linux 操作系統(tǒng)離我們這么近,它是怎么架構(gòu)起來的?往小里說,我們一個(gè)串聯(lián)一個(gè)業(yè)務(wù)請(qǐng)求的數(shù)據(jù)集合,如果使用 BaseSession,XXXSession inherit BaseSession 的設(shè)計(jì),其實(shí),這個(gè)繼承樹,很難適應(yīng)層出不窮的變化。但是如果使用組合,就可以拆解出 UserSignature 等等各種可能需要的部件,在需要的時(shí)候組合使用,不斷添加新的部件而沒有對(duì)老的繼承樹的記憶這個(gè)心智負(fù)擔(dān)。
使用組合,其實(shí)就是要讓你明確清楚自己現(xiàn)在所擁有的是哪個(gè)部件。如果部件過于多,其實(shí)完成組合最終成品這個(gè)步驟,就會(huì)有較高的心智負(fù)擔(dān),每個(gè)部件展開來,琳瑯滿目,眼花繚亂。比如 QT 這個(gè)通用 UI 框架,看它的Class 列表,有 1000 多個(gè)。如果不用繼承樹把它組織起來,平鋪展開,組合出一個(gè)頁(yè)面,將會(huì)變得心智負(fù)擔(dān)高到無法承受。OOP 在'需要無數(shù)元素同時(shí)展現(xiàn)出來'這種復(fù)雜度極高的場(chǎng)景,有效的控制了復(fù)雜度 。'那么,古爾丹,代價(jià)是什么呢?'代價(jià)就是,一開始做出這個(gè)自上而下的設(shè)計(jì),牽一發(fā)而動(dòng)全身,每次調(diào)整都變得異常困難。
實(shí)際項(xiàng)目中,各種職業(yè)級(jí)別不同的同學(xué)一起協(xié)作修改一個(gè) server 的代碼,就會(huì)出現(xiàn),職級(jí)低的同學(xué)改哪里都改不對(duì),根本沒能力進(jìn)行修改,高級(jí)別的同學(xué)能修改對(duì),也不愿意大規(guī)模修改,整個(gè)項(xiàng)目變得愈發(fā)不合理。對(duì)整個(gè)繼承樹沒有完全認(rèn)識(shí)的同學(xué)都沒有資格進(jìn)行任何一個(gè)對(duì)繼承樹有調(diào)整的修改,協(xié)作變得寸步難行。代碼的修改,都變成了依賴一個(gè)高級(jí)架構(gòu)師高強(qiáng)度監(jiān)控繼承體系的變化,低級(jí)別同學(xué)們束手束腳的結(jié)果。組合,就很好的解決了這個(gè)問題,把問題不斷細(xì)分,每個(gè)同學(xué)都可以很好地攻克自己需要攻克的點(diǎn),實(shí)現(xiàn)一個(gè) package。產(chǎn)品邏輯代碼,只需要去組合各個(gè) package,就能達(dá)到效果。
這是 golang 標(biāo)準(zhǔn)庫(kù)里 http request 的定義,它就是 Http 請(qǐng)求所有特性集合出來的結(jié)果。其中通用/異變/多種實(shí)現(xiàn)的部分,通過 duck interface 抽象,比如 Body io.ReadCloser。你想知道哪些細(xì)節(jié),就從組合成 request 的部件入手,要修改,只需要修改對(duì)應(yīng)部件。[這段代碼后,對(duì)比.NET 的 HTTP 基于 OOP 的抽象]
//?A?Request?represents?an?HTTP?request?received?by?a?server
//?or?to?be?sent?by?a?client.
//
//?The?field?semantics?differ?slightly?between?client?and?server
//?usage.?In?addition?to?the?notes?on?the?fields?below,?see?the
//?documentation?for?Request.Write?and?RoundTripper.
type?Request?struct?{
??//?Method?specifies?the?HTTP?method?(GET,?POST,?PUT,?etc.).
??//?For?client?requests,?an?empty?string?means?GET.
??//
??//?Go's?HTTP?client?does?not?support?sending?a?request?with
??//?the?CONNECT?method.?See?the?documentation?on?Transport?for
??//?details.
??Method?string
??//?URL?specifies?either?the?URI?being?requested?(for?server
??//?requests)?or?the?URL?to?access?(for?client?requests).
??//
??//?For?server?requests,?the?URL?is?parsed?from?the?URI
??//?supplied?on?the?Request-Line?as?stored?in?RequestURI.?For
??//?most?requests,?fields?other?than?Path?and?RawQuery?will?be
??//?empty.?(See?RFC?7230,?Section?5.3)
??//
??//?For?client?requests,?the?URL's?Host?specifies?the?server?to
??//?connect?to,?while?the?Request's?Host?field?optionally
??//?specifies?the?Host?header?value?to?send?in?the?HTTP
??//?request.
??URL?*url.URL
??//?The?protocol?version?for?incoming?server?requests.
??//
??//?For?client?requests,?these?fields?are?ignored.?The?HTTP
??//?client?code?always?uses?either?HTTP/1.1?or?HTTP/2.
??//?See?the?docs?on?Transport?for?details.
??Proto?string?//?"HTTP/1.0"
??ProtoMajor?int????//?1
??ProtoMinor?int????//?0
??//?Header?contains?the?request?header?fields?either?received
??//?by?the?server?or?to?be?sent?by?the?client.
??//
??//?If?a?server?received?a?request?with?header?lines,
??//
??//?Host:?example.com
??//?accept-encoding:?gzip,?deflate
??//?Accept-Language:?en-us
??//?fOO:?Bar
??//?foo:?two
??//
??//?then
??//
??//?Header?=?map[string][]string{
??//?"Accept-Encoding":?{"gzip,?deflate"},
??//?"Accept-Language":?{"en-us"},
??//?"Foo":?{"Bar",?"two"},
??//?}
??//
??//?For?incoming?requests,?the?Host?header?is?promoted?to?the
??//?Request.Host?field?and?removed?from?the?Header?map.
??//
??//?HTTP?defines?that?header?names?are?case-insensitive.?The
??//?request?parser?implements?this?by?using?CanonicalHeaderKey,
??//?making?the?first?character?and?any?characters?following?a
??//?hyphen?uppercase?and?the?rest?lowercase.
??//
??//?For?client?requests,?certain?headers?such?as?Content-Length
??//?and?Connection?are?automatically?written?when?needed?and
??//?values?in?Header?may?be?ignored.?See?the?documentation
??//?for?the?Request.Write?method.
??Header?Header
??//?Body?is?the?request's?body.
??//
??//?For?client?requests,?a?nil?body?means?the?request?has?no
??//?body,?such?as?a?GET?request.?The?HTTP?Client's?Transport
??//?is?responsible?for?calling?the?Close?method.
??//
??//?For?server?requests,?the?Request?Body?is?always?non-nil
??//?but?will?return?EOF?immediately?when?no?body?is?present.
??//?The?Server?will?close?the?request?body.?The?ServeHTTP
??//?Handler?does?not?need?to.
??Body?io.ReadCloser
??//?GetBody?defines?an?optional?func?to?return?a?new?copy?of
??//?Body.?It?is?used?for?client?requests?when?a?redirect?requires
??//?reading?the?body?more?than?once.?Use?of?GetBody?still
??//?requires?setting?Body.
??//
??//?For?server?requests,?it?is?unused.
??GetBody?func()?(io.ReadCloser,?error)
??//?ContentLength?records?the?length?of?the?associated?content.
??//?The?value?-1?indicates?that?the?length?is?unknown.
??//?Values?>=?0?indicate?that?the?given?number?of?bytes?may
??//?be?read?from?Body.
??//
??//?For?client?requests,?a?value?of?0?with?a?non-nil?Body?is
??//?also?treated?as?unknown.
??ContentLength?int64
??//?TransferEncoding?lists?the?transfer?encodings?from?outermost?to
??//?innermost.?An?empty?list?denotes?the?"identity"?encoding.
??//?TransferEncoding?can?usually?be?ignored;?chunked?encoding?is
??//?automatically?added?and?removed?as?necessary?when?sending?and
??//?receiving?requests.
??TransferEncoding?[]string
??//?Close?indicates?whether?to?close?the?connection?after
??//?replying?to?this?request?(for?servers)?or?after?sending?this
??//?request?and?reading?its?response?(for?clients).
??//
??//?For?server?requests,?the?HTTP?server?handles?this?automatically
??//?and?this?field?is?not?needed?by?Handlers.
??//
??//?For?client?requests,?setting?this?field?prevents?re-use?of
??//?TCP?connections?between?requests?to?the?same?hosts,?as?if
??//?Transport.DisableKeepAlives?were?set.
??Close?bool
??//?For?server?requests,?Host?specifies?the?host?on?which?the
??//?URL?is?sought.?For?HTTP/1?(per?RFC?7230,?section?5.4),?this
??//?is?either?the?value?of?the?"Host"?header?or?the?host?name
??//?given?in?the?URL?itself.?For?HTTP/2,?it?is?the?value?of?the
??//?":authority"?pseudo-header?field.
??//?It?may?be?of?the?form?"host:port".?For?international?domain
??//?names,?Host?may?be?in?Punycode?or?Unicode?form.?Use
??//?golang.org/x/net/idna?to?convert?it?to?either?format?if
??//?needed.
??//?To?prevent?DNS?rebinding?attacks,?server?Handlers?should
??//?validate?that?the?Host?header?has?a?value?for?which?the
??//?Handler?considers?itself?authoritative.?The?included
??//?ServeMux?supports?patterns?registered?to?particular?host
??//?names?and?thus?protects?its?registered?Handlers.
??//
??//?For?client?requests,?Host?optionally?overrides?the?Host
??//?header?to?send.?If?empty,?the?Request.Write?method?uses
??//?the?value?of?URL.Host.?Host?may?contain?an?international
??//?domain?name.
??Host?string
??//?Form?contains?the?parsed?form?data,?including?both?the?URL
??//?field's?query?parameters?and?the?PATCH,?POST,?or?PUT?form?data.
??//?This?field?is?only?available?after?ParseForm?is?called.
??//?The?HTTP?client?ignores?Form?and?uses?Body?instead.
??Form?url.Values
??//?PostForm?contains?the?parsed?form?data?from?PATCH,?POST
??//?or?PUT?body?parameters.
??//
??//?This?field?is?only?available?after?ParseForm?is?called.
??//?The?HTTP?client?ignores?PostForm?and?uses?Body?instead.
??PostForm?url.Values
??//?MultipartForm?is?the?parsed?multipart?form,?including?file?uploads.
??//?This?field?is?only?available?after?ParseMultipartForm?is?called.
??//?The?HTTP?client?ignores?MultipartForm?and?uses?Body?instead.
??MultipartForm?*multipart.Form
??//?Trailer?specifies?additional?headers?that?are?sent?after?the?request
??//?body.
??//
??//?For?server?requests,?the?Trailer?map?initially?contains?only?the
??//?trailer?keys,?with?nil?values.?(The?client?declares?which?trailers?it
??//?will?later?send.)?While?the?handler?is?reading?from?Body,?it?must
??//?not?reference?Trailer.?After?reading?from?Body?returns?EOF,?Trailer
??//?can?be?read?again?and?will?contain?non-nil?values,?if?they?were?sent
??//?by?the?client.
??//
??//?For?client?requests,?Trailer?must?be?initialized?to?a?map?containing
??//?the?trailer?keys?to?later?send.?The?values?may?be?nil?or?their?final
??//?values.?The?ContentLength?must?be?0?or?-1,?to?send?a?chunked?request.
??//?After?the?HTTP?request?is?sent?the?map?values?can?be?updated?while
??//?the?request?body?is?read.?Once?the?body?returns?EOF,?the?caller?must
??//?not?mutate?Trailer.
??//
??//?Few?HTTP?clients,?servers,?or?proxies?support?HTTP?trailers.
??Trailer?Header
??//?RemoteAddr?allows?HTTP?servers?and?other?software?to?record
??//?the?network?address?that?sent?the?request,?usually?for
??//?logging.?This?field?is?not?filled?in?by?ReadRequest?and
??//?has?no?defined?format.?The?HTTP?server?in?this?package
??//?sets?RemoteAddr?to?an?"IP:port"?address?before?invoking?a
??//?handler.
??//?This?field?is?ignored?by?the?HTTP?client.
??RemoteAddr?string
??//?RequestURI?is?the?unmodified?request-target?of?the
??//?Request-Line?(RFC?7230,?Section?3.1.1)?as?sent?by?the?client
??//?to?a?server.?Usually?the?URL?field?should?be?used?instead.
??//?It?is?an?error?to?set?this?field?in?an?HTTP?client?request.
??RequestURI?string
??//?TLS?allows?HTTP?servers?and?other?software?to?record
??//?information?about?the?TLS?connection?on?which?the?request
??//?was?received.?This?field?is?not?filled?in?by?ReadRequest.
??//?The?HTTP?server?in?this?package?sets?the?field?for
??//?TLS-enabled?connections?before?invoking?a?handler;
??//?otherwise?it?leaves?the?field?nil.
??//?This?field?is?ignored?by?the?HTTP?client.
??TLS?*tls.ConnectionState
??//?Cancel?is?an?optional?channel?whose?closure?indicates?that?the?client
??//?request?should?be?regarded?as?canceled.?Not?all?implementations?of
??//?RoundTripper?may?support?Cancel.
??//
??//?For?server?requests,?this?field?is?not?applicable.
??//
??//?Deprecated:?Set?the?Request's?context?with?NewRequestWithContext
??//?instead.?If?a?Request's?Cancel?field?and?context?are?both
??//?set,?it?is?undefined?whether?Cancel?is?respected.
??Cancel?<-chan?struct{}
??//?Response?is?the?redirect?response?which?caused?this?request
??//?to?be?created.?This?field?is?only?populated?during?client
??//?redirects.
??Response?*Response
??//?ctx?is?either?the?client?or?server?context.?It?should?only
??//?be?modified?via?copying?the?whole?Request?using?WithContext.
??//?It?is?unexported?to?prevent?people?from?using?Context?wrong
??//?and?mutating?the?contexts?held?by?callers?of?the?same?request.
??ctx?context.Context
}
看看.NET 里對(duì)于 web 服務(wù)的抽象,僅僅看到末端,不去看完整個(gè)繼承樹的完整圖景,我根本無法知道我關(guān)心的某個(gè)細(xì)節(jié)在什么位置。進(jìn)而,我要往整個(gè) http 服務(wù)體系里修改任何功能,都無法拋開對(duì)整體完整設(shè)計(jì)的理解和熟悉,還極容易沒有知覺地破壞者整體的設(shè)計(jì)。
說到組合,還有一個(gè)關(guān)系很緊密的詞,叫插件化。大家都用 vscode 用得很開心,它比 visual studio 成功在哪里?如果 vscode 通過添加一堆插件達(dá)到 visual studio 具備的能力,那么它將變成另一個(gè)和 visual studio 差不多的東西,叫做 vs studio 吧。大家應(yīng)該發(fā)現(xiàn)問題了,我們很多時(shí)候其實(shí)并不需要 visual studio 的大多數(shù)功能,而且希望靈活定制化一些比較小眾的能力,用一些小眾的插件。甚至,我們希望選擇不同實(shí)現(xiàn)的同類型插件。這就是組合的力量,各種不同的組合,它簡(jiǎn)單,卻又滿足了各種需求,靈活多變,要實(shí)現(xiàn)一個(gè)插件,不需要事先掌握一個(gè)龐大的體系。體現(xiàn)在代碼上,也是一樣的道理。至少后端開發(fā)領(lǐng)域,組合,比 OOP,'香'很多。
原則 6 吝嗇原則: 除非確無它法, 不要編寫龐大的程序
可能有些同學(xué)會(huì)覺得,把程序?qū)懙谬嫶笠恍┎藕媚玫贸鍪秩ピu(píng) T11、T12。leader 們一看評(píng)審方案就容易覺得:很大,很好,很全面。但是,我們真的需要寫這么大的程序么?
我又要說了"那么,古爾丹,代價(jià)是什么呢?"。代價(jià)是代碼越多,越難維護(hù),難調(diào)整。C 語(yǔ)言之父 Ken Thompson 說"刪除一行代碼,給我?guī)淼某删透幸忍砑右恍幸?。我們對(duì)于代碼,要吝嗇。能把系統(tǒng)做小,就不要做大。騰訊不乏 200w+行的客戶端,很大,很牛。但是,同學(xué)們自問,現(xiàn)在還調(diào)整得動(dòng)架構(gòu)么。手 Q 的同學(xué)們,看看自己代碼,曾經(jīng)嘆息過么。能小做的事情就小做,尋求通用化,通過 duck interface(甚至多進(jìn)程,用于隔離能力的多線程)把模塊、能力隔離開,時(shí)刻想著刪減代碼量,才能保持代碼的可維護(hù)性和面對(duì)未來的需求、架構(gòu),調(diào)整自身的活力。客戶端代碼,UI 渲染模塊可以復(fù)雜吊炸天,非 UI 部分應(yīng)該追求最簡(jiǎn)單,能力接口化,可替換、重組合能力強(qiáng)。
落地到大家的代碼,review 時(shí),就應(yīng)該最關(guān)注核心 struct 定義,構(gòu)建起一個(gè)完備的模型,核心 interface,明確抽象 model 對(duì)外部的依賴,明確抽象 model 對(duì)外提供的能力。其他代碼,就是要用最簡(jiǎn)單、平平無奇的代碼實(shí)現(xiàn)模型內(nèi)部細(xì)節(jié)。
原則 7 透明性原則: 設(shè)計(jì)要可見,以便審查和調(diào)試
首先,定義一下,什么是透明性和可顯性。
"如果沒有陰暗的角落和隱藏的深度,軟件系統(tǒng)就是透明的。透明性是一種被動(dòng)的品質(zhì)。如果實(shí)際上能預(yù)測(cè)到程序行為的全部或大部分情況,并能建立簡(jiǎn)單的心理模型,這個(gè)程序就是透明的,因?yàn)榭梢钥赐笝C(jī)器究竟在干什么。
如果軟件系統(tǒng)所包含的功能是為了幫助人們對(duì)軟件建立正確的'做什么、怎么做'的心理模型而設(shè)計(jì),這個(gè)軟件系統(tǒng)就是可顯的。因此,舉例來說,對(duì)用戶而言,良好的文檔有助于提高可顯性;對(duì)程序員而言,良好的變量和函數(shù)名有助于提高可顯性??娠@性是一種主動(dòng)品質(zhì)。在軟件中要達(dá)到這一點(diǎn),僅僅做到不晦澀是不夠的,還必須要盡力做到有幫助。"
我們要寫好程序,減少 bug,就要增強(qiáng)自己對(duì)代碼的控制力。你始終做到,理解自己調(diào)用的函數(shù)/復(fù)用的代碼大概是怎么實(shí)現(xiàn)的。不然,你可能就會(huì)在單線程狀態(tài)機(jī)的 server 里調(diào)用有 IO 阻塞的函數(shù),讓自己的 server 吞吐量直接掉到底。進(jìn)而,為了保證大家能對(duì)自己代碼能做到有控制力,所有人寫的函數(shù),就必須具備很高的透明性。而不是寫一些看了一陣看不明白的函數(shù)/代碼,結(jié)果被迫使用你代碼的人,直接放棄了對(duì)掌控力的追取,甚至放棄復(fù)用你的代碼,另起爐灶,走向了'制造重復(fù)代碼'的深淵。
透明性其實(shí)相對(duì)容易做到的,大家有意識(shí)地鍛煉一兩個(gè)月,就能做得很好??娠@性就不容易了。有一個(gè)現(xiàn)象是,你寫的每一個(gè)函數(shù)都不超過 80 行,每一行我都能看懂,但是你層層調(diào)用,很多函數(shù)調(diào)用,組合起來怎么就實(shí)現(xiàn)了某個(gè)功能,看兩遍,還是看不懂。第三遍可能才能大概看懂。大概看懂了,但太復(fù)雜,很難在大腦里構(gòu)建起你實(shí)現(xiàn)這個(gè)功能的整體流程。結(jié)果就是,閱讀者根本做不到對(duì)你的代碼有好的掌控力。
可顯性的標(biāo)準(zhǔn)很簡(jiǎn)單,大家看一段代碼,懂不懂,一下就明白了。但是,如何做好可顯性?那就是要追求合理的函數(shù)分組,合理的函數(shù)上下級(jí)層次,同一層次的代碼才會(huì)出現(xiàn)在同一個(gè)函數(shù)里,追求通俗易懂的函數(shù)分組分層方式,是通往可顯性的道路。
當(dāng)然,復(fù)雜如 linux 操作系統(tǒng),office 文檔,問題本身就很復(fù)雜,拆解、分層、組合得再合理,都難建立心理模型。這個(gè)時(shí)候,就需要完備的文檔了。完備的文檔還需要出現(xiàn)在離代碼最近的地方,讓人'知道這里復(fù)雜的邏輯有文檔',而不是其實(shí)文檔,但是閱讀者不知道。再看看上面 golang 標(biāo)準(zhǔn)庫(kù)里的 http.Request,感受到它在可顯性上的努力了么?對(duì),就去學(xué)它。
原則 10 通俗原則: 接口設(shè)計(jì)避免標(biāo)新立異
設(shè)計(jì)程序過于標(biāo)新立異的話,可能會(huì)提升別人理解的難度。
一般,我們這么定義一個(gè)'點(diǎn)',使用 x 表示橫坐標(biāo),用 y 表示縱坐標(biāo):
type?Point?struct?{
?X?float64
?Y?float64
}
你就是要不同、精準(zhǔn):
type?Point?struct?{
?VerticalOrdinate???float64
?HorizontalOrdinate?float64
}
很好,你用詞很精準(zhǔn),一般人還駁斥不了你。但是,多數(shù)人讀你的 VerticalOrdinate 就是沒有讀 X 理解來得快,來得容易懂、方便。你是在刻意制造協(xié)作成本。
上面的例子常見,但還不是最小立異原則最想說明的問題。想想一下,一個(gè)程序里,你把用'+'這個(gè)符號(hào)表示數(shù)組添加元素,而不是數(shù)學(xué)'加','result := 1+2' --> 'result = []int{1, 2}'而不是'result=3',那么,你這個(gè)標(biāo)新立異,對(duì)程序的破壞性,簡(jiǎn)直無法想象。"最小立異原則的另一面是避免表象想死而實(shí)際卻略有不同。這會(huì)極端危險(xiǎn),因?yàn)楸硐笙嗨仆鶎?dǎo)致人們產(chǎn)生錯(cuò)誤的假定。所以最好讓不同事物有明顯區(qū)別,而不要看起來幾乎一模一樣。" -- Henry Spencer。
你實(shí)現(xiàn)一個(gè) db.Add()函數(shù)卻做著 db.AddOrUpdate()的操作,有人使用了你的接口,錯(cuò)誤地把數(shù)據(jù)覆蓋了。
原則 11 緘默原則: 如果一個(gè)程序沒什么好說的,就沉默
這個(gè)原則,應(yīng)該是大家最經(jīng)常破壞的原則之一。一段簡(jiǎn)短的代碼里插入了各種'log("cmd xxx enter")', 'log("req data " + req.String())',非常害怕自己信息打印得不夠。害怕自己不知道程序執(zhí)行成功了,總要最后'log("success")'。但是,我問一下大家,你們真的耐心看過別人寫的代碼打的一堆日志么?不是自己需要哪個(gè),就在一堆日志里,再打印一個(gè)日志出來一個(gè)帶有特殊標(biāo)記的日志'log("this_is_my_log_" + xxxxx)'?結(jié)果,第一個(gè)作者打印的日志,在代碼交接給其他人或者在跟別人協(xié)作的時(shí)候,這個(gè)日志根本沒有價(jià)值,反而提升了大家看日志的難度。
一個(gè)服務(wù)一跑起來,就瘋狂打日志,請(qǐng)求處理正常也打一堆日志。滾滾而來的日志,把錯(cuò)誤日志淹沒在里面。錯(cuò)誤日志失去了效果,簡(jiǎn)單地 tail 查看日志,眼花繚亂,看不出任何問題,這不就成了'為了捕獲問題'而讓自己'根本無法捕獲問題'了么?
沉默是金。除了簡(jiǎn)單的 stat log,如果你的程序'發(fā)聲'了,那么它拋出的信息就一定要有效!打印一個(gè) log('process fail')也是毫無價(jià)值,到底什么 fail 了?是哪個(gè)用戶帶著什么參數(shù)在哪個(gè)環(huán)節(jié)怎么 fail 了?如果發(fā)聲,就要把必要信息給全。不然就是不發(fā)聲,表示自己好好地 work 著呢。不發(fā)聲就是最好的消息,現(xiàn)在我的 work 一切正常!
"設(shè)計(jì)良好的程序?qū)⒂脩舻淖⒁饬σ暈橛邢薜膶氋F資源,只有在必要時(shí)才要求使用。"程序員自己的主力,也是寶貴的資源!只有有必要的時(shí)候,日志才跑來提醒程序員'我有問題,來看看',而且,必須要給到足夠的信息,讓一把講明白現(xiàn)在發(fā)生了什么。而不是程序員還需要很多輔助手段來搞明白到底發(fā)生了什么。
每當(dāng)我發(fā)布程序 ,我抽查一個(gè)機(jī)器,看它的日志。發(fā)現(xiàn)只有每分鐘外部接入、內(nèi)部 rpc 的個(gè)數(shù)/延時(shí)分布日志的時(shí)候,我就心情很愉悅。我知道,這一分鐘,它的成功率又是 100%,沒任何問題!
原則 12 補(bǔ)救原則: 出現(xiàn)異常時(shí),馬上退出并給出足夠錯(cuò)誤信息
其實(shí)這個(gè)問題很簡(jiǎn)單,如果出現(xiàn)異常,異常并不會(huì)因?yàn)槲覀儑L試掩蓋它,它就不存在了。所以,程序錯(cuò)誤和邏輯錯(cuò)誤要嚴(yán)格區(qū)分對(duì)待。這是一個(gè)態(tài)度問題。
'異常是互聯(lián)網(wǎng)服務(wù)器的常態(tài)'。邏輯錯(cuò)誤通過 metrics 統(tǒng)計(jì),我們做好告警分析。對(duì)于程序錯(cuò)誤 ,我們就必須要嚴(yán)格做到在問題最早出現(xiàn)的位置就把必要的信息搜集起來,高調(diào)地告知開發(fā)和維護(hù)者'我出現(xiàn)異常了,請(qǐng)立即修復(fù)我!'??梢允侵苯泳蜎]有被捕獲的 panic 了。也可以在一個(gè)最上層的位置統(tǒng)一做好 recover 機(jī)制,但是在 recover 的時(shí)候一定要能獲得準(zhǔn)確異常位置的準(zhǔn)確異常信息。不能有中間 catch 機(jī)制,catch 之后丟失很多信息再往上傳遞。
很多 Java 開發(fā)的同學(xué),不區(qū)分程序錯(cuò)誤和邏輯錯(cuò)誤,要么都很寬容,要么都很嚴(yán)格,對(duì)代碼的可維護(hù)性是毀滅性的破壞。"我的程序沒有程序錯(cuò)誤,如果有,我當(dāng)時(shí)就解決了。"只有這樣,才能保持程序代碼質(zhì)量的相對(duì)穩(wěn)定,在火苗出現(xiàn)時(shí)撲滅火災(zāi)是最好的撲滅火災(zāi)的方式。當(dāng)然,更有效的方式是全面自動(dòng)化測(cè)試的預(yù)防:)
具體實(shí)踐點(diǎn)
前面提了好多思考方向的問題。大的原則問題和方向。我這里,再來給大家簡(jiǎn)單列舉幾個(gè)細(xì)節(jié)執(zhí)行點(diǎn)吧。畢竟,大家要上手,是從執(zhí)行開始,然后才是總結(jié)思考,能把我的思考方式抄過去。下面是針對(duì) golang 語(yǔ)言的,其他語(yǔ)言略有不同。以及,我一時(shí)也想不全我所執(zhí)行的 所有細(xì)則,這就是我強(qiáng)調(diào)'原則'的重要性,原則是可枚舉的。
對(duì)于代碼格式規(guī)范,100%嚴(yán)格執(zhí)行,嚴(yán)重容不得一點(diǎn)沙。 文件絕不能超過 800 行,超過,一定要思考怎么拆文件。工程思維,就在于拆文件的時(shí)候積累。 函數(shù)對(duì)決不能超過 80 行,超過,一定要思考怎么拆函數(shù),思考函數(shù)分組,層次。工程思維,就在于拆文件的時(shí)候積累。 代碼嵌套層次不能超過 4 層,超過了就得改。多想想能不能 early return。工程思維,就在于拆文件的時(shí)候積累。
if?!needContinue?{
?doA()
?return
}?else?{
?doB()
?return
}
if?!needContinue?{
?doA()
?return
}
doB()
return
下面這個(gè)就是 early return,把兩端代碼從邏輯上解耦了。
從目錄、package、文件、struct、function 一層層下來 ,信息一定不能出現(xiàn)冗余。比如 file.FileProperty 這種定義。只有每個(gè)'定語(yǔ)'只出現(xiàn)在一個(gè)位置,才為'做好邏輯、定義分組/分層'提供了可能性。
多用多級(jí)目錄來組織代碼所承載的信息,即使某一些中間目錄只有一個(gè)子目錄。
隨著代碼的擴(kuò)展,老的代碼違反了一些設(shè)計(jì)原則,應(yīng)該立即原地局部重構(gòu),維持住代碼質(zhì)量不滑坡。比如:拆文件;拆函數(shù);用 Session 來保存一個(gè)復(fù)雜的流程型函數(shù)的所有信息;重新調(diào)整目錄結(jié)構(gòu)。
基于上一點(diǎn)考慮,我們應(yīng)該盡量讓項(xiàng)目的代碼有一定的組織、層次關(guān)系。我個(gè)人的當(dāng)前實(shí)踐是除了特別通用的代碼,都放在一個(gè) git 里。特別通用、修改少的代碼,逐漸獨(dú)立出 git,作為子 git 連接到當(dāng)前項(xiàng)目 git,讓 goland 的 Refactor 特性、各種 Refactor 工具能幫助我們快速、安全局部重構(gòu)。
自己的項(xiàng)目代碼,應(yīng)該有一個(gè)內(nèi)生的層級(jí)和邏輯關(guān)系。flat 平鋪展開是非常不利于代碼復(fù)用的。怎么復(fù)用、怎么組織復(fù)用,肯定會(huì)變成'人生難題'。T4-T7 的同學(xué)根本無力解決這種難題。
如果被 review 的代碼雖然簡(jiǎn)短,但是你看了一眼卻發(fā)現(xiàn)不咋懂,那就一定有問題。自己看不出來,就找高級(jí)別的同學(xué)交流。這是你和別 review 代碼的同學(xué)成長(zhǎng)的時(shí)刻。
日志要少打。要打日志就要把關(guān)鍵索引信息帶上。必要的日志必須打。
有疑問就立即問,不要怕問錯(cuò)。讓代碼作者給出解釋。不要怕問出極低問題。
不要說'建議',提問題,就是剛,你 pk 不過我,就得改!
請(qǐng)積極使用 trpc??偸且屠习逭驹谝黄?!只有和老板達(dá)成的對(duì)于代碼質(zhì)量建設(shè)的共識(shí),才能在團(tuán)隊(duì)里更好地做好代碼質(zhì)量建設(shè)。
消滅重復(fù)!消滅重復(fù)!消滅重復(fù)!
主干開發(fā)
最后,我來為'主干開發(fā)'多說一句話。道理很簡(jiǎn)單,只有每次被 review 代碼不到 500 行,reviewer 才能快速地看完,而且?guī)缀醪粫?huì)看漏。超過 500 行,reviewer 就不能仔細(xì)看,只能大概瀏覽了。而且,讓你調(diào)整 500 行代碼內(nèi)的邏輯比調(diào)整 3000 行甚至更多的代碼,容易很多,降低不僅僅是 6 倍,而是一到兩個(gè)數(shù)量級(jí)。有問題,在剛出現(xiàn)的時(shí)候就調(diào)整了,不會(huì)給被 revew 的人帶來大的修改負(fù)擔(dān)。
關(guān)于 CI(continuous integration),還有很多好的資料和書籍,大家應(yīng)該及時(shí)去學(xué)習(xí)學(xué)習(xí)。
《unix 編程藝術(shù)》
建議大家把這本書找出來讀一讀。特別是,T7 及更高級(jí)別的同學(xué)。你們已經(jīng)積累了大量的代碼實(shí)踐,亟需對(duì)'工程性'做思考總結(jié)。很多工程方法論都過時(shí)了,這本書的內(nèi)容,是例外中的例外。它所表達(dá)出的內(nèi)容沒有因?yàn)檐浖夹g(shù)的不斷更替而過時(shí)。
佛教禪宗講'不立文字'(不立文字,教外別傳,直指人心,見性成佛),很多道理和感悟是不能用文字傳達(dá)的,文字的表達(dá)能力,不能表達(dá)。大家常常因?yàn)?自己聽說過、知道某個(gè)道理"而產(chǎn)生一種安心感,認(rèn)為"我懂了這個(gè)道理",但是自己卻不能在實(shí)踐中做到。知易行難,知道卻做不到,在工程實(shí)踐里,就和'不懂這個(gè)道理'沒有任何區(qū)別了。
曾經(jīng),我面試過一個(gè)別的公司的總監(jiān),講得好像一套一套,代碼拉出來遛一遛,根本就沒做到,僅僅會(huì)道聽途說。他在工程實(shí)踐上的探索前路可以說已經(jīng)基本斷絕了。我只能祝君能做好向上管理,走自己的純管理道路吧。請(qǐng)不要再說自己對(duì)技術(shù)有追求,是個(gè)技術(shù)人了!
所以,大家不僅僅是看看我這篇文章,而是在實(shí)踐中去不斷踐行和積累自己的'教外別傳'吧。
