国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

還敢亂寫(xiě)代碼??騰訊 Code Review 規(guī)范出爐!

共 23231字,需瀏覽 47分鐘

 ·

2020-12-07 03:00

來(lái)源:騰訊技術(shù)工程

前言

作為公司代碼委員會(huì) golang 分會(huì)的理事,我 review 了很多代碼,看了很多別人的 review 評(píng)論。發(fā)現(xiàn)不少同學(xué) code review 與寫(xiě)出好代碼的水平有待提高。在這里,想分享一下我的一些理念和思路。

為什么技術(shù)人員包括 leader 都要做 code review

諺語(yǔ)曰: 'Talk Is Cheap, Show Me The Code'。知易行難,知行合一難。嘴里要講出來(lái)總是輕松,把別人講過(guò)的話記住,組織一下語(yǔ)言,再講出來(lái),很容易。絕知此事要躬行。設(shè)計(jì)理念你可能道聽(tīng)途說(shuō)了一些,以為自己掌握了,但是你會(huì)做么?有能力去思考、改進(jìn)自己當(dāng)前的實(shí)踐方式和實(shí)踐中的代碼細(xì)節(jié)么?不客氣地說(shuō),很多人僅僅是知道并且認(rèn)同了某個(gè)設(shè)計(jì)理念,進(jìn)而產(chǎn)生了一種虛假的安心感---自己的技術(shù)并不差。但是,他根本沒(méi)有去實(shí)踐這些設(shè)計(jì)理念,甚至根本實(shí)踐不了這些設(shè)計(jì)理念,從結(jié)果來(lái)說(shuō),他懂不懂這些道理/理念,有什么差別?變成了自欺欺人。

代碼,是設(shè)計(jì)理念落地的地方,是技術(shù)的呈現(xiàn)和根本。同學(xué)們可以在 review 過(guò)程中做到落地溝通,不再是空對(duì)空的討論,可以在實(shí)際問(wèn)題中產(chǎn)生思考的碰撞,互相學(xué)習(xí),大家都掌握?qǐng)F(tuán)隊(duì)里積累出來(lái)最好的實(shí)踐方式!當(dāng)然,如果 leader 沒(méi)時(shí)間寫(xiě)代碼,僅僅是 review 代碼,指出其他同學(xué)某些實(shí)踐方式不好,要給出好的實(shí)踐的意見(jiàn),即使沒(méi)親手寫(xiě)代碼,也是對(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)目的開(kāi)發(fā)便利性、可維護(hù)性、可測(cè)試性、運(yùn)營(yíng)質(zhì)量。

厲害的技術(shù)人,主要可以分為下面幾個(gè)方向:

  • 奇技淫巧

掌握很多技巧,以及發(fā)現(xiàn)技巧一系列思路,比如很多編程大賽,比的就是這個(gè)。但是,這個(gè)對(duì)工程,用處好像并不是很大。

  • 領(lǐng)域奠基

比如約翰*卡馬克,他創(chuàng)造出了現(xiàn)代計(jì)算機(jī)圖形高效渲染的方法論。不論如果沒(méi)有他,后面會(huì)不會(huì)有人發(fā)明,他就是第一個(gè)發(fā)明了。1999 年,卡馬克登上了美國(guó)時(shí)代雜志評(píng)選出來(lái)的科技領(lǐng)域 50 大影響力人物榜單,并且名列第 10 位。但是,類(lèi)似的殿堂級(jí)位置,沒(méi)有幾個(gè),不夠大家分,沒(méi)我們的事兒。

  • 理論研究

八十年代李開(kāi)復(fù)博士堅(jiān)持采用隱含馬爾可夫模型的框架,成功地開(kāi)發(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é)科,我們自己制造了很多問(wèn)題,進(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)樽铋_(kāi)始協(xié)議設(shè)計(jì)得不好,第一個(gè)使用接口的人,沒(méi)有類(lèi)似上面這個(gè)函數(shù)的代碼,自己實(shí)現(xiàn)了一個(gè)嵌入邏輯代碼的填寫(xiě)請(qǐng)求結(jié)構(gòu)結(jié)構(gòu)體的代碼,一開(kāi)始,挺好的。但當(dāng)有第二個(gè)人,第三個(gè)人干了類(lèi)似的事情,我們將無(wú)法再重構(gòu)這個(gè)協(xié)議,必須做到麻煩的向前兼容。而且每個(gè)同學(xué),都要理解一遍上面這個(gè)協(xié)議怎么填,理解有問(wèn)題,就觸發(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è)。你有面臨過(guò)這樣的'人生難題'么?其實(shí)怎么選并不重要了,你寫(xiě)的這個(gè)代碼已經(jīng)在走向 shit 的道路上邁出了堅(jiān)實(shí)的一步。

但是,A little copying is better than a little dependency。這里提一嘴,不展開(kāi)。

這里,我必須額外說(shuō)一句。大家使用 trpc。感覺(jué)自己被鼓勵(lì)'每個(gè)服務(wù)搞一個(gè) git'。那,你這個(gè)服務(wù)里訪問(wèn) db 的代碼,rpc 的代碼,各種可以復(fù)用的代碼,是用的大家都復(fù)用的 git 下的代碼么?每次都重復(fù)寫(xiě)一遍,db 字段細(xì)節(jié)改了,每個(gè)使用過(guò) db 的 server 對(duì)應(yīng)的 git 都改一遍?這個(gè)通用 git 已經(jīng)寫(xiě)好的接口應(yīng)該不知道哪些 git 下的代碼因?yàn)樽约翰幌蚯凹嫒莸男薷亩肋h(yuǎn)放棄了向前不兼容的修改?

早期有效的決策不再有效

很多時(shí)候,我們第一版代碼寫(xiě)出來(lái),是沒(méi)有太大的問(wèn)題的。比如,下面這個(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)度沒(méi)超過(guò) 80 行,邏輯比價(jià)清晰。但是當(dāng) isMerge 這里判斷邏輯,如果加入更多的邏輯,把局部行數(shù)撐到 50 行以上,這個(gè)函數(shù),味道就壞了。出現(xiàn)兩個(gè)問(wèn)題:

1)函數(shù)內(nèi)代碼不在一個(gè)邏輯層次上,閱讀代碼,本來(lái)在閱讀著頂層邏輯,突然就掉入了長(zhǎng)達(dá) 50 行的 isMerge 的邏輯處理細(xì)節(jié),還沒(méi)看完,讀者已經(jīng)忘了前面的代碼講了什么,需要來(lái)回看,挑戰(zhàn)自己大腦的 cache 尺寸。

2)代碼有問(wèn)題后,再新加代碼的同學(xué),是改還是不改前人寫(xiě)好的代碼呢?出 bug 誰(shuí)來(lái)背?這是一個(gè)靈魂拷問(wèn)。

過(guò)早的優(yōu)化

這個(gè)大家聽(tīng)了很多了,這里不贅述。

對(duì)合理性沒(méi)有苛求

'兩種寫(xiě)法都 ok,你隨便挑一種吧','我這樣也沒(méi)什么吧',這是我經(jīng)常聽(tī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,那,為什么不一開(kāi)始就是 defer,進(jìn)入最合理的狀態(tài)?不一開(kāi)始就進(jìn)入最合理的狀態(tài),在后續(xù)協(xié)作中,其他同學(xué)很可能犯錯(cuò)!

總是面向?qū)ο?總喜歡封裝

我是軟件工程科班出身。學(xué)的第一門(mén)編程語(yǔ)言是 c++。教材是這本 。當(dāng)時(shí)自己讀完教材,初入程序設(shè)計(jì)之門(mén),對(duì)于里面講的'封裝',驚為天人,多么美妙的設(shè)計(jì)啊,面向?qū)ο?,多么智慧的設(shè)計(jì)啊。但是,這些年來(lái),我看到了大牛'云風(fēng)'對(duì)于'畢業(yè)生使用 mysql api 就喜歡搞個(gè) class 封裝再用'的嘲諷;看到了各種莫名其妙的 class 定義;體會(huì)到了經(jīng)常要去看一個(gè)莫名其妙的繼承樹(shù),必須要把整個(gè)繼承樹(shù)整體讀明白才能確認(rèn)一個(gè)細(xì)小的邏輯分支;多次體會(huì)到了我需要辛苦地壓抑住自己的抵觸情緒,去細(xì)度一個(gè)自作聰明的被封裝的代碼,確認(rèn)我的 bug。除了 UI 類(lèi)場(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è)抽象層次上,不通讀代碼,沒(méi)人能讀明白。我想確認(rèn)任何細(xì)節(jié),都要把多個(gè)層次的代碼都通讀了,有什么封裝性可言?

好,你說(shuō)是作者沒(méi)有把 class name 取得好。那,問(wèn)題是,你能取得好么?一個(gè)剛?cè)肼毜?T1.2 的同學(xué)能把 class name、class 樹(shù)設(shè)計(jì)得好么?即使是對(duì)簡(jiǎn)單的業(yè)務(wù)模型,也需要無(wú)數(shù)次'壞'的對(duì)象抽象實(shí)踐,才能培養(yǎng)出一個(gè)具有合格的 class 抽象能力的同學(xué),這對(duì)于大型卻松散的團(tuán)隊(duì)協(xié)作,不是破壞性的?已經(jīng)有了一套繼承樹(shù),想要添加功能就只能在這個(gè)繼承樹(shù)里添加,以前的繼承樹(shù)不再適合新的需求,這個(gè)繼承樹(shù)上所有的 class,以及使用它們的地方,你都去改?不,是個(gè)正常人都會(huì)放棄,開(kāi)始堆屎山。

封裝,就是我可以不關(guān)心實(shí)現(xiàn)。但是,做一個(gè)穩(wěn)定的系統(tǒng),每一層設(shè)計(jì)都可能出問(wèn)題。abi,總有合適的用法和不合適的用法,真的存在我們能完全不關(guān)心封裝的部分是怎么實(shí)現(xiàn)的?不,你不能。bug 和性能問(wèn)題,常常就出現(xiàn)在,你用了錯(cuò)誤的用法去使用一個(gè)封裝好的函數(shù)。即使是 android、ios 的 api,golang、java 現(xiàn)成的 api,我們常常都要去探究實(shí)現(xiàn),才能把 api 用好。

那,我們是不是該一上來(lái),就做一個(gè)透明性很強(qiáng)的函數(shù),才更為合理?使用者想知道細(xì)節(jié),進(jìn)來(lái)吧,我的實(shí)現(xiàn)很易讀,你看看就明白,使用時(shí)不會(huì)迷路!對(duì)于邏輯復(fù)雜的函數(shù),我們還要強(qiáng)調(diào)函數(shù)內(nèi)部工作方式'可以讓讀者在大腦里想象呈現(xiàn)完整過(guò)程'的可現(xiàn)性,讓使用者輕松讀懂,有把握,使用時(shí),不迷路!

根本沒(méi)有設(shè)計(jì)

這個(gè)最可怕,所有需求,上手就是一頓擼,'設(shè)計(jì)是什么東西?我一個(gè)文件 5w 行,一個(gè)函數(shù) 5k 行,干不完需求?'從第一行代碼開(kāi)始,就是無(wú)設(shè)計(jì)的,隨意地踩著滿(mǎn)地的泥坑,對(duì)于旁人的眼光沒(méi)有感覺(jué),一個(gè)人獨(dú)舞,產(chǎn)出的代碼,完成了需求,毀滅了接手自己代碼的人。這個(gè)就不舉例了,每個(gè)同學(xué)應(yīng)該都能在自己的項(xiàng)目類(lèi)發(fā)現(xiàn)這種代碼。

必須形而上的思考

常常,同學(xué)們聽(tīng)演講,公開(kāi)課,就喜歡聽(tīng)一些細(xì)枝末節(jié)的'干活'。這沒(méi)有問(wèn)題。但是,你干了幾年活,學(xué)習(xí)了多少干貨知識(shí)點(diǎn)?構(gòu)建起自己的技術(shù)思考'面',進(jìn)入立體的'工程思維',把技術(shù)細(xì)節(jié)和系統(tǒng)要滿(mǎn)足的需求在思考上連接起來(lái)了么?當(dāng)聽(tīng)一個(gè)需求的時(shí)候,你能思考到自己的 code package 該怎么組織,函數(shù)該怎么組織了么?

那,技術(shù)點(diǎn)要怎么和需求連接起來(lái)呢?答案很簡(jiǎn)單,你需要在時(shí)間里總結(jié),總結(jié)出一些明確的原則、思維過(guò)程。思考怎么去總結(jié),特別像是在思考哲學(xué)問(wèn)題。從一些瑣碎的細(xì)節(jié)中,由具體情況上升到一些原則、公理。同時(shí),大家在接受原則時(shí),不應(yīng)該是接受和記住原則本身,而應(yīng)該是結(jié)構(gòu)原則,讓這個(gè)原則在自己這里重新推理一遍,自己完全掌握這個(gè)原則的適用范圍。

再進(jìn)一步具體地說(shuō),對(duì)于工程最佳實(shí)踐的形而上的思考過(guò)程,就是:

把工程實(shí)踐中遇到的問(wèn)題,從問(wèn)題類(lèi)型和解法類(lèi)型,兩個(gè)角度去歸類(lèi),總結(jié)出一些有限適用的原則,就從點(diǎn)到了面。把諸多總結(jié)出的原則,組合應(yīng)用到自己的項(xiàng)目代碼中,就是把多個(gè)面結(jié)合起來(lái)構(gòu)建了一套立體的最佳實(shí)踐的方案。當(dāng)你這套方案能適應(yīng) 30w+行代碼的項(xiàng)目,超過(guò) 30 人的項(xiàng)目,你就架構(gòu)師入門(mén)了!當(dāng)你這個(gè)項(xiàng)目,是多端,多語(yǔ)言,代碼量超過(guò) 300w 行,參與人數(shù)超過(guò) 300 人,代碼質(zhì)量依然很高,代碼依然在高效地自我迭代,每天消除掉過(guò)時(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)目。沒(méi)啥好說(shuō)的,你已經(jīng)是專(zhuān)家工程師了。級(jí)別再高,我就不了解了,不在這里討論。

那么,我們要重頭開(kāi)始積累思考和總結(jié)?不,有一本書(shū)叫做《unix 編程藝術(shù)》,我在不同的時(shí)期分別讀了 3 遍,等一會(huì),我講一些里面提到的,我覺(jué)得在騰訊尤其值得拿出來(lái)說(shuō)的原則。這些原則,正好就能作為 code review 時(shí)大家判定代碼質(zhì)量的準(zhǔn)繩。但,在那之前,我得講一下另外一個(gè)很重要的話題,模型設(shè)計(jì)。

model 設(shè)計(jì)

沒(méi)讀過(guò) oauth2.0 RFC,就去設(shè)計(jì)第三方授權(quán)登陸的人,終歸還要再發(fā)明一個(gè)撇腳的 oauth。

2012 年我剛畢業(yè),我和一個(gè)去了廣州聯(lián)通公司的華南理工畢業(yè)生聊天。當(dāng)時(shí)他說(shuō)他工作很不開(kāi)心,因?yàn)楣ぷ骼锊唤?jīng)常寫(xiě)代碼,而且認(rèn)為自己有 ACM 競(jìng)賽金牌級(jí)的算法熟練度+對(duì) CPP 代碼的熟悉,寫(xiě)下一個(gè)個(gè)指針操作內(nèi)存,什么程序?qū)懖怀鰜?lái),什么事情做不好。當(dāng)時(shí)我覺(jué)得,挺有道理,編程工具在手,我什么事情做不了?

現(xiàn)在,我會(huì)告訴他,復(fù)雜如 linux 操作系統(tǒng)、Chromium 引擎、windows office,你做不了。原因是,他根本沒(méi)進(jìn)入軟件工程的工程世界。不是會(huì)搬磚就能修出港珠澳大橋。但是,這么回答并不好,舉證用的論據(jù)離我們太遙遠(yuǎn)了。見(jiàn)微知著。我現(xiàn)在會(huì)回答,你做不了,簡(jiǎn)單如一個(gè)權(quán)限系統(tǒng),你知道怎么做么?堆積一堆邏輯層次一維展開(kāi)的 if else?簡(jiǎn)單如一個(gè)共享文件管理,你知道怎么做么?堆積一堆邏輯層次一維展開(kāi)的 ife lse?你聯(lián)通有上萬(wàn)臺(tái)服務(wù)器,你要怎么寫(xiě)一個(gè)管理平臺(tái)?堆積一堆邏輯層次一維展開(kāi)的 ife lse?

上來(lái)就是干,能實(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 說(shuō)的,沒(méi)有良好的領(lǐng)域思考和模型抽象,邏輯復(fù)雜度就是 n^2 指數(shù)級(jí)的,你得寫(xiě)多少 ifelse,得思考多少可能的 if 路徑,來(lái) cover 所有的不合符預(yù)期的情況。你必須要有 Domain 思考探索、model 拆解/抽象/構(gòu)建的能力。

有人問(wèn)過(guò)我,要怎么有效地獲得這個(gè)能力?這個(gè)問(wèn)題我沒(méi)能回答,就像是在問(wèn)我,怎么才能獲得 MIT 博士的學(xué)術(shù)能力?我無(wú)法回答。唯一回答就是,進(jìn)入某個(gè)領(lǐng)域,就是首先去看前人的思考,站在前人的肩膀上,再用上自己的通識(shí)能力,去進(jìn)一步思考。至于怎么建立好的通識(shí)思考能力,可能得去常青藤讀個(gè)書(shū)吧:)或者,就在工程實(shí)踐中思考和鍛煉自己的這個(gè)能力!

同時(shí),基于 model 設(shè)計(jì)的代碼,能更好地適應(yīng)產(chǎn)品經(jīng)理不斷變更的需求。比如說(shuō),一個(gè) calendar(日歷)應(yīng)用,簡(jiǎn)單來(lái)想,不要太簡(jiǎn)單!以'userid_date'為 key 記錄一個(gè)用戶(hù)的每日安排不就完成了么?只往前走一步,設(shè)計(jì)了一個(gè)任務(wù),上限分發(fā)給 100w 個(gè)人,創(chuàng)建這么一個(gè)任務(wù),是往 100w 個(gè)人下面添加一條記錄?你得改掉之前的設(shè)計(jì),換 db。再往前走一步,要拉出某個(gè)用戶(hù)和某個(gè)人一起要參與的所有事務(wù),是把兩個(gè)人的所有任務(wù)來(lái)做 join?好像還行。如果是和 100 個(gè)人一起參與的所有任務(wù)呢?100 個(gè)人的任務(wù)來(lái) join?不現(xiàn)實(shí)了吧。

好,你引入一個(gè)群組 id,那么,你最開(kāi)始的'userid_date'為 key 的設(shè)計(jì),是不是又要修改和做數(shù)據(jù)遷移了?經(jīng)常來(lái)一個(gè)需求,你就得把系統(tǒng)推翻重來(lái),或者根本就只能拒絕用戶(hù)的需求,這樣的戰(zhàn)斗力,還好意思叫自己工程師?你一開(kāi)始就應(yīng)該思考自己面對(duì)的業(yè)務(wù)領(lǐng)域,思考自己的日歷應(yīng)用可能的模型邊界,把可能要做的能力都拿進(jìn)來(lái)思考,構(gòu)建一個(gè) model,設(shè)計(jì)一套通用的 store 層接口,基于通用接口的邏輯代碼。當(dāng)產(chǎn)品不斷發(fā)展,就是不停往模型里填內(nèi)容,而不是推翻重來(lái)。

這,思考模型邊界,構(gòu)建模型細(xì)節(jié),就是兩個(gè)很重要的能力,也是絕大多數(shù)騰訊產(chǎn)品經(jīng)理不具備的能力,你得具備,對(duì)整個(gè)團(tuán)隊(duì)都是極其有益的。你面對(duì)產(chǎn)品經(jīng)理時(shí),就聽(tīng)取他們出于對(duì)用戶(hù)體驗(yàn)負(fù)責(zé)思考出的需求點(diǎn),到你自己這里,用一個(gè)完整的模型去涵蓋這些零碎的點(diǎn)。

model 設(shè)計(jì),是形而上思考中的一個(gè)方面,一個(gè)特別重要的方面。接下來(lái),我們來(lái)抄襲抄襲 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è)專(zhuān)業(yè)人員的素養(yǎng)組成來(lái)說(shuō),有些不成文的行業(yè)素養(yǎng)具有與標(biāo)準(zhǔn)手冊(cè)及教科書(shū)同等重要的地位(并且隨著專(zhuān)業(yè)人員經(jīng)驗(yàn)的日積月累,這些經(jīng)驗(yàn)常常會(huì)比書(shū)本更重要)。資深工程師們?cè)诠ぷ髦袝?huì)積累大量的隱性知識(shí),他們用類(lèi)似禪宗'教外別傳'的方式,通過(guò)言傳身教傳授給后輩。軟件工程算是此規(guī)則的一個(gè)例外:技術(shù)變革如此之快,軟件環(huán)境日新月異,軟件技術(shù)文化暫如朝露。

然而,例外之中也有例外。確有極少數(shù)軟件技術(shù)被證明經(jīng)久耐用,足以演進(jìn)為強(qiáng)勢(shì)的技術(shù)文化、有鮮明特色的藝術(shù)和世代相傳的設(shè)計(jì)哲學(xué)?!?/p>

接下來(lái),我用我的理解,講解一下幾個(gè)我們常常做不到的原則。

Keep It Simple Stuped!

KISS 原則,大家應(yīng)該是如雷貫耳了。但是,你真的在遵守?什么是 Simple?簡(jiǎn)單?golang 語(yǔ)言主要設(shè)計(jì)者之一的 Rob Pike 說(shuō)'大道至簡(jiǎn)',這個(gè)'簡(jiǎn)'和簡(jiǎn)單是一個(gè)意思么?

首先,簡(jiǎn)單不是面對(duì)一個(gè)問(wèn)題,我們印入眼簾第一映像的解法為簡(jiǎn)單。我說(shuō)一句,感受一下。"把一個(gè)事情做出來(lái)容易,把事情用最簡(jiǎn)單有效的方法做出來(lái),是一個(gè)很難的事情。"比如,做一個(gè)三方授權(quán),oauth2.0 很簡(jiǎn)單,所有概念和細(xì)節(jié)都是緊湊、完備、易用的。

你覺(jué)得要設(shè)計(jì)到 oauth2.0 這個(gè)效果很容易么?要做到簡(jiǎn)單,就要對(duì)自己處理的問(wèn)題有全面的了解,然后需要不斷積累思考,才能做到從各個(gè)角度和層級(jí)去認(rèn)識(shí)這個(gè)問(wèn)題,打磨出一個(gè)通俗、緊湊、完備的設(shè)計(jì),就像 ios 的交互設(shè)計(jì)。簡(jiǎn)單不是容易做到的,需要大家在不斷的時(shí)間和 code review 過(guò)程中去積累思考,pk 中觸發(fā)思考,交流中總結(jié)思考,才能做得愈發(fā)地好,接近'大道至簡(jiǎn)'。

兩張經(jīng)典的模型圖,簡(jiǎn)單又全面,感受一下,沒(méi)看懂,可以立即自行 google 學(xué)習(xí)一下:RBAC:

logging:

原則 3 組合原則: 設(shè)計(jì)時(shí)考慮拼接組合

關(guān)于 OOP,關(guān)于繼承,我前面已經(jīng)說(shuō)過(guò)了。那我們?cè)趺唇M織自己的模塊?對(duì),用組合的方式來(lái)達(dá)到。linux 操作系統(tǒng)離我們這么近,它是怎么架構(gòu)起來(lái)的?往小里說(shuō),我們一個(gè)串聯(lián)一個(gè)業(yè)務(wù)請(qǐng)求的數(shù)據(jù)集合,如果使用 BaseSession,XXXSession inherit BaseSession 的設(shè)計(jì),其實(shí),這個(gè)繼承樹(shù),很難適應(yīng)層出不窮的變化。但是如果使用組合,就可以拆解出 UserSignature 等等各種可能需要的部件,在需要的時(shí)候組合使用,不斷添加新的部件而沒(méi)有對(duì)老的繼承樹(shù)的記憶這個(gè)心智負(fù)擔(dān)。

使用組合,其實(shí)就是要讓你明確清楚自己現(xiàn)在所擁有的是哪個(gè)部件。如果部件過(guò)于多,其實(shí)完成組合最終成品這個(gè)步驟,就會(huì)有較高的心智負(fù)擔(dān),每個(gè)部件展開(kāi)來(lái),琳瑯滿(mǎn)目,眼花繚亂。比如 QT 這個(gè)通用 UI 框架,看它的Class 列表,有 1000 多個(gè)。如果不用繼承樹(shù)把它組織起來(lái),平鋪展開(kāi),組合出一個(gè)頁(yè)面,將會(huì)變得心智負(fù)擔(dān)高到無(wú)法承受。OOP 在'需要無(wú)數(shù)元素同時(shí)展現(xiàn)出來(lái)'這種復(fù)雜度極高的場(chǎng)景,有效的控制了復(fù)雜度 。'那么,古爾丹,代價(jià)是什么呢?'代價(jià)就是,一開(kāi)始做出這個(gè)自上而下的設(shè)計(jì),牽一發(fā)而動(dòng)全身,每次調(diào)整都變得異常困難。

實(shí)際項(xiàng)目中,各種職業(yè)級(jí)別不同的同學(xué)一起協(xié)作修改一個(gè) server 的代碼,就會(huì)出現(xiàn),職級(jí)低的同學(xué)改哪里都改不對(duì),根本沒(méi)能力進(jìn)行修改,高級(jí)別的同學(xué)能修改對(duì),也不愿意大規(guī)模修改,整個(gè)項(xiàng)目變得愈發(fā)不合理。對(duì)整個(gè)繼承樹(shù)沒(méi)有完全認(rèn)識(shí)的同學(xué)都沒(méi)有資格進(jìn)行任何一個(gè)對(duì)繼承樹(shù)有調(diào)整的修改,協(xié)作變得寸步難行。代碼的修改,都變成了依賴(lài)一個(gè)高級(jí)架構(gòu)師高強(qiáng)度監(jiān)控繼承體系的變化,低級(jí)別同學(xué)們束手束腳的結(jié)果。組合,就很好的解決了這個(gè)問(wèn)題,把問(wèn)題不斷細(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)求所有特性集合出來(lái)的結(jié)果。其中通用/異變/多種實(shí)現(xiàn)的部分,通過(guò) 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è)繼承樹(shù)的完整圖景,我根本無(wú)法知道我關(guān)心的某個(gè)細(xì)節(jié)在什么位置。進(jìn)而,我要往整個(gè) http 服務(wù)體系里修改任何功能,都無(wú)法拋開(kāi)對(duì)整體完整設(shè)計(jì)的理解和熟悉,還極容易沒(méi)有知覺(jué)地破壞者整體的設(shè)計(jì)。

說(shuō)到組合,還有一個(gè)關(guān)系很緊密的詞,叫插件化。大家都用 vscode 用得很開(kāi)心,它比 visual studio 成功在哪里?如果 vscode 通過(guò)添加一堆插件達(dá)到 visual studio 具備的能力,那么它將變成另一個(gè)和 visual studio 差不多的東西,叫做 vs studio 吧。大家應(yīng)該發(fā)現(xiàn)問(wèn)題了,我們很多時(shí)候其實(shí)并不需要 visual studio 的大多數(shù)功能,而且希望靈活定制化一些比較小眾的能力,用一些小眾的插件。甚至,我們希望選擇不同實(shí)現(xiàn)的同類(lèi)型插件。這就是組合的力量,各種不同的組合,它簡(jiǎn)單,卻又滿(mǎn)足了各種需求,靈活多變,要實(shí)現(xiàn)一個(gè)插件,不需要事先掌握一個(gè)龐大的體系。體現(xiàn)在代碼上,也是一樣的道理。至少后端開(kāi)發(fā)領(lǐng)域,組合,比 OOP,'香'很多。

原則 6 吝嗇原則: 除非確無(wú)它法, 不要編寫(xiě)龐大的程序

可能有些同學(xué)會(huì)覺(jué)得,把程序?qū)懙谬嫶笠恍┎藕媚玫贸鍪秩ピu(píng) T11、T12。leader 們一看評(píng)審方案就容易覺(jué)得:很大,很好,很全面。但是,我們真的需要寫(xiě)這么大的程序么?

我又要說(shuō)了"那么,古爾丹,代價(jià)是什么呢?"。代價(jià)是代碼越多,越難維護(hù),難調(diào)整。C 語(yǔ)言之父 Ken Thompson 說(shuō)"刪除一行代碼,給我?guī)?lái)的成就感要比添加一行要大"。我們對(duì)于代碼,要吝嗇。能把系統(tǒng)做小,就不要做大。騰訊不乏 200w+行的客戶(hù)端,很大,很牛。但是,同學(xué)們自問(wèn),現(xiàn)在還調(diào)整得動(dòng)架構(gòu)么。手 Q 的同學(xué)們,看看自己代碼,曾經(jīng)嘆息過(guò)么。能小做的事情就小做,尋求通用化,通過(guò) duck interface(甚至多進(jìn)程,用于隔離能力的多線程)把模塊、能力隔離開(kāi),時(shí)刻想著刪減代碼量,才能保持代碼的可維護(hù)性和面對(duì)未來(lái)的需求、架構(gòu),調(diào)整自身的活力。客戶(hù)端代碼,UI 渲染模塊可以復(fù)雜吊炸天,非 UI 部分應(yīng)該追求最簡(jiǎn)單,能力接口化,可替換、重組合能力強(qiáng)。

落地到大家的代碼,review 時(shí),就應(yīng)該最關(guān)注核心 struct 定義,構(gòu)建起一個(gè)完備的模型,核心 interface,明確抽象 model 對(duì)外部的依賴(lài),明確抽象 model 對(duì)外提供的能力。其他代碼,就是要用最簡(jiǎn)單、平平無(wú)奇的代碼實(shí)現(xiàn)模型內(nèi)部細(xì)節(jié)。

原則 7 透明性原則: 設(shè)計(jì)要可見(jiàn),以便審查和調(diào)試

首先,定義一下,什么是透明性和可顯性。

"如果沒(méi)有陰暗的角落和隱藏的深度,軟件系統(tǒng)就是透明的。透明性是一種被動(dòng)的品質(zhì)。如果實(shí)際上能預(yù)測(cè)到程序行為的全部或大部分情況,并能建立簡(jiǎn)單的心理模型,這個(gè)程序就是透明的,因?yàn)榭梢钥赐笝C(jī)器究竟在干什么。

如果軟件系統(tǒng)所包含的功能是為了幫助人們對(duì)軟件建立正確的'做什么、怎么做'的心理模型而設(shè)計(jì),這個(gè)軟件系統(tǒng)就是可顯的。因此,舉例來(lái)說(shuō),對(duì)用戶(hù)而言,良好的文檔有助于提高可顯性;對(duì)程序員而言,良好的變量和函數(shù)名有助于提高可顯性??娠@性是一種主動(dòng)品質(zhì)。在軟件中要達(dá)到這一點(diǎn),僅僅做到不晦澀是不夠的,還必須要盡力做到有幫助。"

我們要寫(xiě)好程序,減少 bug,就要增強(qiáng)自己對(duì)代碼的控制力。你始終做到,理解自己調(diào)用的函數(shù)/復(fù)用的代碼大概是怎么實(shí)現(xiàn)的。不然,你可能就會(huì)在單線程狀態(tài)機(jī)的 server 里調(diào)用有 IO 阻塞的函數(shù),讓自己的 server 吞吐量直接掉到底。進(jìn)而,為了保證大家能對(duì)自己代碼能做到有控制力,所有人寫(xiě)的函數(shù),就必須具備很高的透明性。而不是寫(xiě)一些看了一陣看不明白的函數(shù)/代碼,結(jié)果被迫使用你代碼的人,直接放棄了對(duì)掌控力的追取,甚至放棄復(fù)用你的代碼,另起爐灶,走向了'制造重復(fù)代碼'的深淵。

透明性其實(shí)相對(duì)容易做到的,大家有意識(shí)地鍛煉一兩個(gè)月,就能做得很好。可顯性就不容易了。有一個(gè)現(xiàn)象是,你寫(xiě)的每一個(gè)函數(shù)都不超過(guò) 80 行,每一行我都能看懂,但是你層層調(diào)用,很多函數(shù)調(diào)用,組合起來(lái)怎么就實(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 文檔,問(wèn)題本身就很復(fù)雜,拆解、分層、組合得再合理,都難建立心理模型。這個(gè)時(shí)候,就需要完備的文檔了。完備的文檔還需要出現(xiàn)在離代碼最近的地方,讓人'知道這里復(fù)雜的邏輯有文檔',而不是其實(shí)文檔,但是閱讀者不知道。再看看上面 golang 標(biāo)準(zhǔn)庫(kù)里的 http.Request,感受到它在可顯性上的努力了么?對(duì),就去學(xué)它。

原則 10 通俗原則: 接口設(shè)計(jì)避免標(biāo)新立異

設(shè)計(jì)程序過(guò)于標(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 就是沒(méi)有讀 X 理解來(lái)得快,來(lái)得容易懂、方便。你是在刻意制造協(xié)作成本。

上面的例子常見(jiàn),但還不是最小立異原則最想說(shuō)明的問(wèn)題。想想一下,一個(gè)程序里,你把用'+'這個(gè)符號(hào)表示數(shù)組添加元素,而不是數(shù)學(xué)'加','result := 1+2' --> 'result = []int{1, 2}'而不是'result=3',那么,你這個(gè)標(biāo)新立異,對(duì)程序的破壞性,簡(jiǎn)直無(wú)法想象。"最小立異原則的另一面是避免表象想死而實(shí)際卻略有不同。這會(huì)極端危險(xiǎn),因?yàn)楸硐笙嗨仆鶎?dǎo)致人們產(chǎn)生錯(cuò)誤的假定。所以最好讓不同事物有明顯區(qū)別,而不要看起來(lái)幾乎一模一樣。" -- Henry Spencer。

你實(shí)現(xiàn)一個(gè) db.Add()函數(shù)卻做著 db.AddOrUpdate()的操作,有人使用了你的接口,錯(cuò)誤地把數(shù)據(jù)覆蓋了。

原則 11 緘默原則: 如果一個(gè)程序沒(méi)什么好說(shuō)的,就沉默

這個(gè)原則,應(yīng)該是大家最經(jīng)常破壞的原則之一。一段簡(jiǎn)短的代碼里插入了各種'log("cmd xxx enter")', 'log("req data " + req.String())',非常害怕自己信息打印得不夠。害怕自己不知道程序執(zhí)行成功了,總要最后'log("success")'。但是,我問(wèn)一下大家,你們真的耐心看過(guò)別人寫(xiě)的代碼打的一堆日志么?不是自己需要哪個(gè),就在一堆日志里,再打印一個(gè)日志出來(lái)一個(gè)帶有特殊標(biāo)記的日志'log("this_is_my_log_" + xxxxx)'?結(jié)果,第一個(gè)作者打印的日志,在代碼交接給其他人或者在跟別人協(xié)作的時(shí)候,這個(gè)日志根本沒(méi)有價(jià)值,反而提升了大家看日志的難度。

一個(gè)服務(wù)一跑起來(lái),就瘋狂打日志,請(qǐng)求處理正常也打一堆日志。滾滾而來(lái)的日志,把錯(cuò)誤日志淹沒(méi)在里面。錯(cuò)誤日志失去了效果,簡(jiǎn)單地 tail 查看日志,眼花繚亂,看不出任何問(wèn)題,這不就成了'為了捕獲問(wèn)題'而讓自己'根本無(wú)法捕獲問(wèn)題'了么?

沉默是金。除了簡(jiǎn)單的 stat log,如果你的程序'發(fā)聲'了,那么它拋出的信息就一定要有效!打印一個(gè) log('process fail')也是毫無(wú)價(jià)值,到底什么 fail 了?是哪個(gè)用戶(hù)帶著什么參數(shù)在哪個(gè)環(huán)節(jié)怎么 fail 了?如果發(fā)聲,就要把必要信息給全。不然就是不發(fā)聲,表示自己好好地 work 著呢。不發(fā)聲就是最好的消息,現(xiàn)在我的 work 一切正常!

"設(shè)計(jì)良好的程序?qū)⒂脩?hù)的注意力視為有限的寶貴資源,只有在必要時(shí)才要求使用。"程序員自己的主力,也是寶貴的資源!只有有必要的時(shí)候,日志才跑來(lái)提醒程序員'我有問(wèn)題,來(lái)看看',而且,必須要給到足夠的信息,讓一把講明白現(xiàn)在發(fā)生了什么。而不是程序員還需要很多輔助手段來(lái)搞明白到底發(fā)生了什么。

每當(dāng)我發(fā)布程序 ,我抽查一個(gè)機(jī)器,看它的日志。發(fā)現(xiàn)只有每分鐘外部接入、內(nèi)部 rpc 的個(gè)數(shù)/延時(shí)分布日志的時(shí)候,我就心情很愉悅。我知道,這一分鐘,它的成功率又是 100%,沒(méi)任何問(wèn)題!

原則 12 補(bǔ)救原則: 出現(xiàn)異常時(shí),馬上退出并給出足夠錯(cuò)誤信息

其實(shí)這個(gè)問(wèn)題很簡(jiǎn)單,如果出現(xiàn)異常,異常并不會(huì)因?yàn)槲覀儑L試掩蓋它,它就不存在了。所以,程序錯(cuò)誤和邏輯錯(cuò)誤要嚴(yán)格區(qū)分對(duì)待。這是一個(gè)態(tài)度問(wèn)題。

'異常是互聯(lián)網(wǎng)服務(wù)器的常態(tài)'。邏輯錯(cuò)誤通過(guò) metrics 統(tǒng)計(jì),我們做好告警分析。對(duì)于程序錯(cuò)誤 ,我們就必須要嚴(yán)格做到在問(wèn)題最早出現(xiàn)的位置就把必要的信息搜集起來(lái),高調(diào)地告知開(kāi)發(fā)和維護(hù)者'我出現(xiàn)異常了,請(qǐng)立即修復(fù)我!'??梢允侵苯泳蜎](méi)有被捕獲的 panic 了。也可以在一個(gè)最上層的位置統(tǒng)一做好 recover 機(jī)制,但是在 recover 的時(shí)候一定要能獲得準(zhǔn)確異常位置的準(zhǔn)確異常信息。不能有中間 catch 機(jī)制,catch 之后丟失很多信息再往上傳遞。

很多 Java 開(kāi)發(fā)的同學(xué),不區(qū)分程序錯(cuò)誤和邏輯錯(cuò)誤,要么都很寬容,要么都很?chē)?yán)格,對(duì)代碼的可維護(hù)性是毀滅性的破壞。"我的程序沒(méi)有程序錯(cuò)誤,如果有,我當(dāng)時(shí)就解決了。"只有這樣,才能保持程序代碼質(zhì)量的相對(duì)穩(wěn)定,在火苗出現(xiàn)時(shí)撲滅火災(zāi)是最好的撲滅火災(zāi)的方式。當(dāng)然,更有效的方式是全面自動(dòng)化測(cè)試的預(yù)防:)

具體實(shí)踐點(diǎn)

前面提了好多思考方向的問(wèn)題。大的原則問(wèn)題和方向。我這里,再來(lái)給大家簡(jiǎn)單列舉幾個(gè)細(xì)節(jié)執(zhí)行點(diǎn)吧。畢竟,大家要上手,是從執(zhí)行開(kāi)始,然后才是總結(jié)思考,能把我的思考方式抄過(guò)去。下面是針對(duì) golang 語(yǔ)言的,其他語(yǔ)言略有不同。以及,我一時(shí)也想不全我所執(zhí)行的 所有細(xì)則,這就是我強(qiáng)調(diào)'原則'的重要性,原則是可枚舉的。

  • 對(duì)于代碼格式規(guī)范,100%嚴(yán)格執(zhí)行,嚴(yán)重容不得一點(diǎn)沙。

  • 文件絕不能超過(guò) 800 行,超過(guò),一定要思考怎么拆文件。工程思維,就在于拆文件的時(shí)候積累。

  • 函數(shù)對(duì)決不能超過(guò) 80 行,超過(guò),一定要思考怎么拆函數(shù),思考函數(shù)分組,層次。工程思維,就在于拆文件的時(shí)候積累。

  • 代碼嵌套層次不能超過(guò) 4 層,超過(guò)了就得改。多想想能不能 early return。工程思維,就在于拆文件的時(shí)候積累。

if?!needContinue?{
?doA()
?return
}?else?{
?doB()
?return
}
if?!needContinue?{
?doA()
?return
}

doB()
return

下面這個(gè)就是 early return,把兩端代碼從邏輯上解耦了。

  • 從目錄、package、文件、struct、function 一層層下來(lái) ,信息一定不能出現(xiàn)冗余。比如 file.FileProperty 這種定義。只有每個(gè)'定語(yǔ)'只出現(xiàn)在一個(gè)位置,才為'做好邏輯、定義分組/分層'提供了可能性。

  • 多用多級(jí)目錄來(lái)組織代碼所承載的信息,即使某一些中間目錄只有一個(gè)子目錄。

  • 隨著代碼的擴(kuò)展,老的代碼違反了一些設(shè)計(jì)原則,應(yīng)該立即原地局部重構(gòu),維持住代碼質(zhì)量不滑坡。比如:拆文件;拆函數(shù);用 Session 來(lái)保存一個(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 平鋪展開(kāi)是非常不利于代碼復(fù)用的。怎么復(fù)用、怎么組織復(fù)用,肯定會(huì)變成'人生難題'。T4-T7 的同學(xué)根本無(wú)力解決這種難題。

  • 如果被 review 的代碼雖然簡(jiǎn)短,但是你看了一眼卻發(fā)現(xiàn)不咋懂,那就一定有問(wèn)題。自己看不出來(lái),就找高級(jí)別的同學(xué)交流。這是你和別 review 代碼的同學(xué)成長(zhǎng)的時(shí)刻。

  • 日志要少打。要打日志就要把關(guān)鍵索引信息帶上。必要的日志必須打。

  • 有疑問(wèn)就立即問(wèn),不要怕問(wèn)錯(cuò)。讓代碼作者給出解釋。不要怕問(wèn)出極低問(wèn)題。

  • 不要說(shuō)'建議',提問(wèn)題,就是剛,你 pk 不過(guò)我,就得改!

  • 請(qǐng)積極使用 trpc??偸且屠习逭驹谝黄?!只有和老板達(dá)成的對(duì)于代碼質(zhì)量建設(shè)的共識(shí),才能在團(tuán)隊(duì)里更好地做好代碼質(zhì)量建設(shè)。

  • 消滅重復(fù)!消滅重復(fù)!消滅重復(fù)!

主干開(kāi)發(fā)

最后,我來(lái)為'主干開(kāi)發(fā)'多說(shuō)一句話。道理很簡(jiǎn)單,只有每次被 review 代碼不到 500 行,reviewer 才能快速地看完,而且?guī)缀醪粫?huì)看漏。超過(guò) 500 行,reviewer 就不能仔細(xì)看,只能大概瀏覽了。而且,讓你調(diào)整 500 行代碼內(nèi)的邏輯比調(diào)整 3000 行甚至更多的代碼,容易很多,降低不僅僅是 6 倍,而是一到兩個(gè)數(shù)量級(jí)。有問(wèn)題,在剛出現(xiàn)的時(shí)候就調(diào)整了,不會(huì)給被 revew 的人帶來(lái)大的修改負(fù)擔(dān)。

關(guān)于 CI(continuous integration),還有很多好的資料和書(shū)籍,大家應(yīng)該及時(shí)去學(xué)習(xí)學(xué)習(xí)。

《unix 編程藝術(shù)》

建議大家把這本書(shū)找出來(lái)讀一讀。特別是,T7 及更高級(jí)別的同學(xué)。你們已經(jīng)積累了大量的代碼實(shí)踐,亟需對(duì)'工程性'做思考總結(jié)。很多工程方法論都過(guò)時(shí)了,這本書(shū)的內(nèi)容,是例外中的例外。它所表達(dá)出的內(nèi)容沒(méi)有因?yàn)檐浖夹g(shù)的不斷更替而過(guò)時(shí)。

佛教禪宗講'不立文字'(不立文字,教外別傳,直指人心,見(jiàn)性成佛),很多道理和感悟是不能用文字傳達(dá)的,文字的表達(dá)能力,不能表達(dá)。大家常常因?yàn)?自己聽(tīng)說(shuō)過(guò)、知道某個(gè)道理"而產(chǎn)生一種安心感,認(rèn)為"我懂了這個(gè)道理",但是自己卻不能在實(shí)踐中做到。知易行難,知道卻做不到,在工程實(shí)踐里,就和'不懂這個(gè)道理'沒(méi)有任何區(qū)別了。

曾經(jīng),我面試過(guò)一個(gè)別的公司的總監(jiān),講得好像一套一套,代碼拉出來(lái)遛一遛,根本就沒(méi)做到,僅僅會(huì)道聽(tīng)途說(shuō)。他在工程實(shí)踐上的探索前路可以說(shuō)已經(jīng)基本斷絕了。我只能祝君能做好向上管理,走自己的純管理道路吧。請(qǐng)不要再說(shuō)自己對(duì)技術(shù)有追求,是個(gè)技術(shù)人了!

所以,大家不僅僅是看看我這篇文章,而是在實(shí)踐中去不斷踐行和積累自己的'教外別傳'吧。

題外話: 目前小哈正在個(gè)人博客(新搭建的網(wǎng)站,域名就是犬小哈的拼音)?www.quanxiaoha.com?上更新《Go語(yǔ)言教程》、《Gin Web框架教程》,畢竟Go自帶天然的并發(fā)優(yōu)勢(shì),后端的同學(xué)還是要學(xué)一下的,這個(gè)教程系列小哈會(huì)一直更新下去,歡迎小伙伴們?cè)L問(wèn)哦~

END


有熱門(mén)推薦?

1.?CPU緩存L1/L2/L3工作原理

2.?2020 最爛密碼 TOP 200 大曝光!

3.?緩存與數(shù)據(jù)庫(kù)一致性問(wèn)題深度剖析

4.?后端Coder如何做好代碼設(shè)計(jì)?

最近面試BAT,整理一份面試資料Java面試BATJ通關(guān)手冊(cè),覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)等等。

獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù)?Java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。

文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。

謝謝支持喲 (*^__^*)

瀏覽 41
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)
評(píng)論
圖片
表情
推薦
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 HEYZO少婦AV無碼精品| 久久国产精彩视频| 欧美日韩视频在线播放| 北条麻妃一区二区三区在线观看| 偷拍-91爱爱| 欧美成人黄色电影| 午夜天堂在线| 成人精品免费视频| 丝袜久久| 外国一级片| 免费观看黄色在线视频| 最好看的MV中文字幕国语| 综合久久av| 五月天激情综合网| 久久婷婷影院| 欧美日韩第一页| 久草视频在线免费| 亚洲日本高清| 色色丁香五月天| 日本精品中文字幕| 2025天天操夜夜操| 各种妇女撒尿mm毛免费网站| 国产91黄色| 欧美在线免费观看| 人人人人人妻| 亚洲无码一二三区| 大荫蒂精品另类| 成人影音先锋| 精品一区二区免费视频| 青青草免费公开视频| 日韩無码专区| 丁香六月激情| 中文字幕A片| 欧美成人在线观看| 99久久夜色精品国产亚洲| 日本高清色清di免费观看| AV福利在线| 日本久久人体视频| 综合色婷婷一区二区亚洲欧美国产| 偷窥丶亚洲丶熟女| 欧美日韩高清无码| 天天看天天色| 在线国产视频| 久久免费视频6| 久久永久免费精品人妻专区| 特级A级毛片| 日韩A级视频| 久久午夜视频| 亚洲一级黄色大片| 一见钟情的韩国电影| 国产3区| 九九热精品视频99| 九九九成人| 啊哈嗯| 伊人久久免费视频| 91.n| 国产无码av| 在线观看免费国产| 妹子色综合| 欧美五月婷婷| 亚洲精品久久久久久久久蜜桃| 18禁裸体美女| 91精品国产综合久久久蜜臀九色 | 一本久久综合亚洲鲁鲁五月天 | 人人操天天操| 熟女人妻一区二区| AV网站入口| 国产综合精品久久久久成人AV| 三级AV在线观看| 伊人乱伦| 婷婷夜色福利网| 少妇BBBB| 狼友在线视频| a视频| 久久五月天综合| 性爱无码| 激情丁香六月| 天天干天天日天天干天天日| 亚洲午夜视频| 国产17c精品视频一二三区| 国产色婷婷精品综合在线播放| 成人网站在线看| 亚洲无码大全| 久久群交| 成人国产片女人爽到高潮| 免费AV大全| 无码中文在线| 成人黄色在线观看视频| 天堂中文在线资源| av片在线观看| 一级黄色A片视频| 国产嫩草久久久一二三久久免费观看| 亚洲精品播放| 成年视频网站| 国外操逼视频| 午夜性爱福利| 天天肏天天干| 欧美特级视频| 无套内射免费视频| 日韩在线视频观看| 亚洲欧洲精品在线| 成人免费A片在线观看直播96| 日韩aaa视频| 欧美日韩免费看| 麻豆三级片在线观看| 高清无码小视频| 农村新婚夜一级A片| 久久夜色精品国产欧美乱极品| 亚洲AV无码精品久久一区二区| 91成人免费在线观看| 91精东传媒果冻传媒| 亚洲无码精品在线观看| 天堂av在线免费观看| 五月天丁香婷婷视频| 亚洲成人免费网站| 久久这里精品| 无码一区二区黑人猛烈视频网站| 亚洲欧美日韩无码| 水蜜桃视频在线观看| 亚洲永久视频| 日韩AV一级| 懂色av蜜臀av粉嫩av分| 青娱乐亚洲精品视频| 狠狠狠狠狠| 蜜桃BBwBBWBBwBBw| 黄色片视频| 无码波多野结衣| 毛片av在线| 亚洲成人中文字幕| 欧美在线一区二区三区| 人妻成人网| 91国内精品视频| 亚洲免费观看在线观看| 香蕉av在线播放| 亚洲欧美中文字幕| www.五月婷婷| 日韩AV在线免费| h片网站在线观看| 日本无码成人片在线播放| 香蕉一级视频| AV天堂影视在线观看| 日韩欧美中文字幕在线观看| 成人三级电影在线观看| 色呦呦在线| 黄片无码视频| av一区在线观看| 操B视频在线免费观看| 国内成人精品网站| 成人三级片在线观看| 五月天久久久| 午夜无码av| 成人四区| 强奸乱伦五月天| 日本亲子乱婬一级A片| www在线视频| 国产精品久久| 无码熟妇| www香蕉成人片com| 日逼黄色| 东京热在线免费观看| 精品久久99| 久操视频免费在线观看| 人人澡视频| 国产高清无码福利| 男人天堂资源网| 九九综合精品| 精品国产午夜福利| 水多多成人网站A片| 亚洲成人福利在线| 黑人内射人妖| 中文字幕亚洲精品| 青春草在线视频| 久艹在线| A无码| 国产在线成人| 特黄AV| 奇米色色色| 一卡二卡无码| 六月婷婷久久| 91欧美日韩综合| 丝袜人妻| 日本黄色视频网址| 日韩性爱视频网站| 久久精品视频99| 粉嫩av懂色av蜜臀av分享| 欧美一区二区三区成人片下载 | 一区二区不卡| 亚洲三级无码| 亚洲无码精品视频| 91人人操| 欧美三级在线| 久久永久免费精品人妻专区| 九九r在线精品观看视频| 国产三级黄色AV| 阿宾MD0165麻豆沈娜娜| 青草社区在线观看| 欧美午夜精品一区二区蜜桃 | 久久性视频| 99热1| 亚洲秘无码一区二区三区欧美| 九九色综合| 啪啪啪免费网站| 韩国精精品视频| 乱码中文字幕日韩欧美在线| 2025中文在线观看最好看的电影 | 俺去俺来也在线www色官网| 一区在线视频| 亚洲日本三级片| 欧美视频色| 日韩成人无码片| 国产精品一区在线| 国产小视频在线播放| 99热5| 国产靠逼视频| 国产欧美精品| 国产做受91| 中文字幕无码AV| 四虎永久在线精品无码| 啪啪成人网| 日皮视频网站| 麻豆熟妇乱妇熟色A片在线看 | 99er这里只有精品| aV无码av天天aV天天爽第一| 亚洲第一成网站| 久久成人影音| 在线观看老湿视频福利| 久久五月丁香| 骚逼操| 天天操中文字幕| 狼友在线视频| 狠狠香蕉| 国产一级做a爱免费视频| 精产国品一区二区| 丁香色婷婷五月天| 欧美日韩中文字幕无码| 天天日天天射天天干| 午夜精品18| 久久免费在线视频| 日皮视频| 久久国产一级片| 成人尤物网站| 日韩性爱网址| 青春草在线视频免费观看| 亚洲日本无码50p| 免费A级黄片| BBw日本熟妇BBwHD| 91伊人久热精品| 秘蜜桃色一区二区三区在线观看| AAA片视频| 国产乱╳╳AⅤ毛片| 中文无码观看| 制服无码| 伊人大综合| 日韩国产一区二区| 91av免费观看| 91精品久久久久| 中文一区二区| 天堂中文字幕在线观看| 欧美成人视频电影无码高清| 国产在线观看一区| 久久久久久久久久久久久久久久久久免费精品分类视频 | 亚洲大片免费看| AV免费在线播放| 国产不卡在线| 国产偷拍精品视频| 一级黄色片免费观看| 2014av天堂网| 日韩黄频| 午夜成人福利| 伊人综合干| 国产精品秘ThePorn| 成人黄色av| 丁香五月激情视频| 午夜国产精品AV| 亚洲国产一| 国产豆花视频| 暗呦网一区二区三区| 一区二区免费视频| 青娱乐成人在线视频| 中文字幕高清| 久久天天操| 日韩中文字幕无码| jizz在线视频| 欧美黄色三级视频| 91成人精品视频| gogogo视频在线观看黑人| 午夜一级| 337P人体美鮑高清| 国产亚洲视频在线观看| 黄色av无码| 蜜桃人妻无码AV天堂三区| 久久五月丁香| 久热精品视频| 黄色搞逼视频| 久久99久久99| 操B视频网站| 五月天婷婷在线播放视频免费观看 | 欧美A片网站| 欧美一二三区| 国产精品自拍一区| 五月丁香视频在线观看| 九九九在线| 色色色色色色网站| eeuss久久| 草莓视频在线播放| 爱爱帝国综合社区| 美女性爱视频网站| 欧美一级婬片AAAA毛片| 国产清纯可爱美女自卫裸贷偷情| 亚洲免费观看高清完| 四虎永久在线精品| 高颜值呻吟给力| 久久综合无码内射国产| 国产福利一区二区| 成人精品午夜无码免费| 久草视频在线资源| 好逼天天有| 成人做爰100片免费观看视频| 中文字幕在线观看视频www| 国产91综合一区在线观看| 青青草成人AV| 久操免费在线视频| 天干天干天夜夜操| 精品成人在线观看| 色汉综合| 一道本在线| 91精品国产乱码久久久久| 日韩一级片免费看| 久久丝袜| 中文字幕无码乱伦| 无码1区| 婷婷丁香激情五月天| 99久久婷婷国产综合精品青牛牛 | 午夜不卡视频| 国产日韩欧美综合精品在线观看| 亚洲中文中出| 天天综合天天做天天综合| 韩国精精品视频| 一级a一级a免费观看免免黄‘/| 在线你懂得| 亚洲一区| 在线se| 躁BBB躁BBB添BBBBBB| 成人无码视频在线| 3d动漫精品一区二区三区在线观看| 国产逼| 小黄片高清无码| 欧美大骚逼| 神马午夜精品96| 99精品9| 色热热| 亚洲高清无码电影| 欧美熟妇一区二区三区| 日本AⅤ在线| 精品无码一区二区三区| 中文无码高清视频| 激情性爱五月天| 人妻熟女视频| jizz麻豆| 日本一级特黄电影| 激情五月激情综合网| 西西人体WW大胆无码| 午夜视频无码| 伊人综合网站| 成人免费毛片果冻日本| 国产美女一级真毛片酒店| 日本无码一区二区三三| 在线看v片| 免费亲子乱婬一级A片| 欧美日韩中文在线观看| 强伦轩一区二区三区四区| 日逼一级| 亚洲日韩欧美一区二区天天天| 强伦轩人妻一区二区三区70后| 四季AV之日韩人妻无码| 99热精品在线播放| 亚洲精品aaa| 91av在线免费观看| 午夜福利sw| 国产AV一二三区| 久久久亚洲AV| 国产3区| 久久无码黄片| 激情五月天导航| 精品乱码一区| 欧美va在线| 少妇在厨房| 国内精品一区二区三区| 亚色网址| 亚洲最大黄色视频| 在线观看av中文字幕| 好男人WWW一区二区三区| 麻豆蜜桃wwww精品无码| 亚洲无套内射| 一级做a视频| 亚洲无码在线免费| 欧美成人一级a片| 久操免费在线视频| 日日夜夜天天综合| 欧美性猛交XXXX乱大交蜜桃 | 国产精品不卡在线观看| 亚洲日操| 亚洲熟女少妇| 桃色Av| AV无码免费观看| 国产黄色在线看| 五月天黄色网| 真人BBwBBWBBw另类视频| 亚洲精品91| 日韩在线视频一区二区三区| 丁香五月天在线播放| 天天综合干| 熟女人妻一区二区三区免费看| 小黄片免费在线观看| 成人高清在线| 国产福利在线播放| 国产Av影视| 粉嫩AV蜜乳AV蜜臀AV蜂腰AV | 69无码| 中日毛片| 黄色一级生活片| 欧美三级欧美一级| 成人污污视频| 成人免费观看的毛视频| 激情五月婷婷丁香| 手机看片福利视频| 国产一区二区AV| 91豆花成人社区| 欧美在线不卡综合| 九九九免费| 2017天天射| 东京热男人的天堂| 亚洲免费成人网| 日韩美女做爱| 久久亚洲AV| 欧美69p| 乱伦无码高清麻豆视频一区二区| 麻豆av在线观看| 欧美一级特黄真人做受| 在线观看视频一区| 婷婷色AV| 久久久久久久久毛片| 99热综合| 一级黄色网| 超碰97成人| 好吊视频一区二区三区| 91色综合| 欧美黄色一级网站| 操逼国产| 亚洲视频在线免费播放| 操逼视频在线免费看| 国产无套进入免费| 国产黄色免费乱伦片| 国产免费av网站| 亚洲s在线| 口爆av| 色噜| 又爽又黄免费网站97双女| 中文字幕无码AV| 成人午夜黄片| 人妻毛片| 国产欧美综合精品| 中文字幕无码Av在线看| 狼人一区二区| 丁香花免费高清视频小说完整| 日韩操逼网| 女人18片毛片60分钟黃菲菲| 黄色视频在线观看地址| 国产A片精品| 国产激情在线播放| 成人高清无码在线观看| 日本A∨在线| 人妻japanesewoman| 91性视频| 日一日射一射| 欧美国产精品一区二区三区| 色综合网址| 91视频精品| 欧美精品第一页| 夜夜干天天操| 国产精品系列视频| 人人操人人摸人人干| 日本a在线观看| 国产视频999| 黄色成人网站大全| 中文字幕免费视频在线播放| 不卡AV在线| 97人妻无码| 91亚洲国产成人久久精品网站| 成人国产AV网站| 97人妻| 91爱爱视频| 国产AV黄片| 免费无码蜜臀在线观看| 内射网站在线观看| 国产无遮挡又黄又爽在线观看| 操比视频| 亚洲在线视频播放| 免费在线观看黄视频| 欧美色图888| www.jiujiujiu| 好吊一区二区| 久操网站| 久久黄网| 色婷婷色99国产综合精品| 毛片黄色视频| 国产精品码ls字幕影视| 大香蕉中文视频| 俺也去俺去啦| 在线观看av网站中文字幕| 黄片视频免费在线观看| 九色PORNY9l原创自拍| 京熱大亂交无碼大亂交| 一区二区三区精品无码| 婷婷五月天色综合| 久热这里只有| 五月欧美激情| 色多多毛片| 国产欧美日韩在线| 日韩色情视频| 亚洲视频中文| 一区视频在线| 国产3区| 在线播放内射| 91最新地址| 中文字幕在线永久| 日本99视频| 成年人黄色视频免费观看| 超碰在线人人干| 91视频在线观看| 亚洲AV无码成人精品| 免费看黃色AAAAAA片| 丁香六月综合| 操碰视频在线| 亚洲www在线观看| 中文字幕无码成人| 自拍偷拍图区| 大地资源第三页在线观看免费播放最新 | 人人操人人妻| 久热中文| 视频一区中文字幕| 久久一道本| 亚洲无码视频一区| 亚洲视频免费完整版在线播放| 五月丁香免费视频| 新妺妺窝窝777777野外| 日韩欧美V| 日本久久精品18| 日韩无码成人片| 草草浮力院| 7777精品伊人久久7777| 日韩极品在线观看| 2026AV天堂网| 97爱爱网| 久久大香蕉网| 亚洲免费a| 大香蕉网在线| 中国黄色学生妹一级片| 欧美一区二区三区免费| 国产不卡一区| 免费a视频在线观看| 粉嫩av懂色av蜜臀av分享| 日韩中文字幕人妻| 夜夜操天天操| 日韩一级黄| 91精品人妻一区二区三区蜜桃| 欧美日韩黄片| 露脸偷拍AV2025| 狠狠干五月| www.wuma| 国产精品视频导航| 日韩精品在线一区| 五月丁香视频在线观看| 亚洲在线网站| 黄色成人网站在线播放| 日本不卡中文字幕| 88av在线| 亚洲任你操超碰在线| 成人无码视频在线| 三级无码视频在线观看| 中文字幕一级A片高清免| 色婷婷久久综合| 欧美操BB| 中文字幕黄色电影| 中文字幕第5页| 国产日韩欧美在线| 人人操人人操人人操人人操人人操| 国产特黄视频| 加勒比一区二区三区| 内射婷婷| 一区二区成人电影| 欧美性猛交XXXX乱大交3| 日韩人妻中文字幕| 天堂网在线视频| 骚逼视频聊天记录| 中文字幕69| 五月丁香六月色| 免费中文字幕av| 国产精品视频久久久| 欧美日韩激情| 欧美性久久久久| 91麻豆精品A片国产在线观看| 人妖和人妖互交性XXXX视频| 一级黄色视频片| 蜜臀久久99精品久久久兰草影视| 精品欧美一区二区精品久久| 欧美日日日| 国产淫乱视频| 日韩三级片在线播放| 亚洲一级视频在线观看| 丝袜足交视频| 中文电视剧字幕在线播放网站| 午夜AV免费| 国产亚洲精品午夜福利巨大软件| 黄网站在线播放| 国产精品综合激情| 亚洲无码人妻| 欧美日韩国产一区二区三区| 抽插视频欧美| AV-ThePorn| 中文字幕在线视频第一页| 亚洲一区在线免费观看| 亚洲久热| 四季AV一区二区凹凸懂色桃花| 国产成人一区二区无码| 激情五月天黄色| 亚洲乱淫| 操B五月天| 91美女在线视频| 日韩久久高清| 国产成人无码区免费AV片在线| 性色网站| 精品国产免费无码久久噜噜噜AV| 久久精品国产亚洲AV成人婷婷| 国产免费黄色片| 亚洲欧美成人| 国产黄色片在线免费观看| 人人爱人人摸人人操| 中文天堂| 日韩在线视频网| 日逼网站视频| 内射极品美女| 国产顶级理伦| 国产一区二区三区四区在线观看 | 中文字幕中文| 日韩无码黄片| 欧美精品秘一区二区三区蜜臀| 蜜桃导航-精品导航| 天天躁夜夜躁av| 成人国产AV| 久草黄色电影| AV无码一区二区三区| 丁香色五月婷婷| 国产免费AV在线| 欧美三级长视频| 99精品国自产在线| 俺来也AV| 亚洲无码免费观看| 一级草逼| 日韩成人一区| 五丁香在线观看AV| 在线免费观看av片| 一级a免一级a做免费线看内祥| 久久这里都是精品| 人人干人人操人人摸| 欧美一级网站| youjizzcom日本| 五月天婷婷在线视频| 精品无码人妻一区二区三区 | 国产小黄片在线| 中文字幕人妻在线中文乱码怎么解决| 久久天天操| 午夜亚洲AV永久无码精品蜜芽| 中文字幕在线码| 黄色片毛片| 波多野结衣av在线观看窜天猴| 亚洲AV无码国产精品久久不卡| 国精品伦一区一区三区有限公司| 国产日韩欧美综合精品在线观看 | 无码人妻丰满熟妇精品| 手机免费AV| 人人妻人人玩澡人人爽| 黄网站在线播放| 亚洲无码激情| 欧美熟妇另类久久久久久不卡| 最新中文字幕免费MV第一季歌词| 免费在线观看黄片| 日韩精品在线一区| 17c.白丝喷水自慰| 亚洲精品无码在线播放| 熟女少妇一区二区三区| 色久综合| 日韩精品极品视频在线观看免费| 丝袜足交视频| www.wuma| JULIA超乳JULIA无码| 久操网站| 国产精品无码免费| 91视频一区二区三区| 无码精品一区二区免费| 美女91视频网站| 成人做爰100部片视频| 欧美精品在线观看视频| 东京热无码视频| 亚洲综合色网站| 秋霞中文字幕| wwwxx在线观看| 成人在线欧美| 亚洲AV在线人妻| 波多野结衣高清视频| 精品国产AV| 欧美成人电影在线观看| 九色91视频| 麻豆秘在线观看国产| 全国最大成人网| 欧美草比视频| 亚洲AV高清| 69伊人| 99热这里只有精品1| 天天综合网久久| 免费无人区一码二码乱码怎么办| 亚洲国产精品成人网站| 亚洲射| 免费无码在线看| 可以免费看AV的网站| 丁香五月激情中文字幕| 国产精品国三级国产a11| 天堂素人| 亚洲视频黄色| 精品一区二区三区毛片| 香蕉在线观看| 蜜臀一区二区三区| 18网站视频| www欧美| 激情五月天激情网| 色婷婷视频在线| 麻豆网站91| 青草青视频| 日韩视频中文字幕在线| 国产一页| 国产成人精品片| 91av在线免费观看| av東熱激情东京热| 欧美成人精品AAA| 午夜日逼网站| 欧美日逼视频| 超碰在线无码| 日韩一级性爱| 国产又色又爽又黄又免费| 日产精品久久久| 无码免费在线视频| 在线免费观看a| 久久99久久99久久| 免费A网站| 青青操天天干| 午夜av在线| 99热国产在线观看| 亚洲无吗在线视频| 潮喷av| 亚洲精品白浆高清久久久久久| 久操久操| 亚洲国产精品二二三三区| 日韩中文字幕无码人妻| 国产在线观看av| 亲子伦一区二区三区观看方式| 亚洲色影院| 97人妻天天摸天天爽天天| 黄色网址在线观看视频| 唐山熟女工棚嗷嗷叫| 黄色免费一级片| 999久久久久| 高清无码三级| 免费在线观看视频黄| 欧美精品毛片| 无码一区二区三区在线| 日韩激情视频在线观看| 久久综合中文字幕| 吹潮喷水高潮HD| 国产A片精品| 91视频在| 中文A片| 黄色大片在线免费观看| 国产美女一级真毛片酒店| 丰满的人妻一区二区10| 91偷拍视频| 国产视频1区| 日本视频在线免费| 久久人妻| 一本色道久久综合狠狠躁的推荐| 操逼黄视频| 强奸校花到高潮| 国产精品国产精品国产专区不卡| 91精品久| AV在线播放中文字幕| 欧美五月婷婷| 人人操人人摸人人看| 人妻黄色| 悠悠无码一区日韩妇女| 欧美一级婬片免费视频华泰老添妇| 日日搔AV一区二区三区| TokyoKot大交乱无码| 水蜜桃视频网| 中文一区| 曰曰操| 国产精品AV网站| 亚洲免费观看高清| 国产不卡一区| 我想看操逼| 亚洲日韩网站| AV毛片| 人妻无码一区二区三区摄像头| anwuye官方网站| 天天看天天色| 日日操天天操夜夜操| 国产在线第一页| 激情五月在线| 国产高清小视频| 特黄特色一级特黄大片| 国产欧美综合视频| 水蜜桃一区| 中文字幕日韩高清| 俺来俺去www色官网| 一级aa视频| 先锋影音一区二区三区| 亚洲日韩AV在线| 久久久久亚洲AV成人网人人软件| 在线免费看黄网站| 国产色婷婷精品综合在线播放| 国产A片免费视频| 国产高清无码在线| 噜噜噜av| 狠狠干b| 美女91网站色| 99视频+国产日韩欧美| 俺来也俺去也www色| 97色综合| 日韩视频免费| 五月丁香免费视频| 日韩在线视频免费播放| 大香蕉久久伊人| 亚洲无码一区二区三区妃光 | 成人视频在线播放| 成人在线黄色| 五月丁香婷婷激情| 中文字幕在线观看有码| 成年片免费观看网站免费观看,亚洲+欧... | 第一福利成人AV导航| 日韩在线视频中文字幕| 青青草原成人视频| 国产精品视频在线播放| 国产寡妇亲子伦一区二区三区四区 | 大香蕉中文在线| 亚洲天堂2015| 日本不卡在线视频| 玖玖资源网站| 免费成人一级片| 夜夜骑夜夜撸| 亚洲精品自拍视频| 综合色综合| 久草视频在线资源| 吴梦梦《女教师时间暂停》| 国产无码免费视频| 91精品人妻一区二区三区蜜桃欧美 | 丁香一区二区| 亚洲五月激情| 猫咪亚洲AV成人无码电影| 亚州AV天堂| 2025四虎在线视频观看| 亚洲高清中文字幕| AV在线免费播放| 狠狠操综合网| 日韩一级黄| 在线视频一区二区三区| 婷婷五月天黄色| 亚洲色图成人网| 日本五十路熟女视频| 大香蕉中文网| 欧美成人精品欧美一级| anwuye官方网站| 色综合加勒比| h片网站在线观看| 大香蕉网在线| 最新中文字幕av| 九一av| 在线亚洲色图| 午夜福利视频网站| 黄a在线| aaa国产| 国色天香网站| 91国产乱伦| 一级片a片| av网站在线免费观看| 91AV在线电影| 日韩精品一二区| 亚洲精品无码a片| 先锋影音中文字幕| 一区二区三区视频在线| 国产小电影在线| 中文字幕无码视频在线观看| 国产又爽又黄在线看视频|