在騰訊如何做 Code Review_第1頁
在騰訊如何做 Code Review_第2頁
在騰訊如何做 Code Review_第3頁
在騰訊如何做 Code Review_第4頁
在騰訊如何做 Code Review_第5頁
已閱讀5頁,還剩29頁未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)

文檔簡介

在騰訊,如何做CodeReview前言作為公司代碼委員會golang分會的理事,我review了很多代碼,看了很多別人的review評論。發(fā)現(xiàn)不少同學(xué)codereview與寫出好代碼的水平有待提高。在這里,想分享一下我的一些理念和思路。為什么技術(shù)人員包括leader都要做codereview諺語曰:'TalkIsCheap,ShowMeTheCode'。知易行難,知行合一難。嘴里要講出來總是輕松,把別人講過的話記住,組織一下語言,再講出來,很容易。絕知此事要躬行。設(shè)計理念你可能道聽途說了一些,以為自己掌握了,但是你會做么?有能力去思考、改進(jìn)自己當(dāng)前的實(shí)踐方式和實(shí)踐中的代碼細(xì)節(jié)么?不客氣地說,很多人僅僅是知道并且認(rèn)同了某個設(shè)計理念,進(jìn)而產(chǎn)生了一種虛假的安心感---自己的技術(shù)并不差。但是,他根本沒有去實(shí)踐這些設(shè)計理念,甚至根本實(shí)踐不了這些設(shè)計理念,從結(jié)果來說,他懂不懂這些道理/理念,有什么差別?變成了自欺欺人。代碼,是設(shè)計理念落地的地方,是技術(shù)的呈現(xiàn)和根本。同學(xué)們可以在review過程中做到落地溝通,不再是空對空的討論,可以在實(shí)際問題中產(chǎn)生思考的碰撞,互相學(xué)習(xí),大家都掌握團(tuán)隊(duì)里積累出來最好的實(shí)踐方式!當(dāng)然,如果leader沒時間寫代碼,僅僅是review代碼,指出其他同學(xué)某些實(shí)踐方式不好,要給出好的實(shí)踐的意見,即使沒親手寫代碼,也是對最佳實(shí)踐要有很多思考。為什么同學(xué)們要在review中思考和總結(jié)最佳實(shí)踐我這里先給一個我自己的總結(jié):所謂架構(gòu)師,就是掌握大量設(shè)計理念和原則、落地到各種語言及附帶工具鏈(生態(tài))下的實(shí)踐方法、垂直行業(yè)模型理解,定制系統(tǒng)模型設(shè)計和工程實(shí)踐規(guī)范細(xì)則。進(jìn)而控制30+萬行代碼項(xiàng)目的開發(fā)便利性、可維護(hù)性、可測試性、運(yùn)營質(zhì)量。厲害的技術(shù)人,主要可以分為下面幾個方向:奇技淫巧掌握很多技巧,以及發(fā)現(xiàn)技巧一系列思路,比如很多編程大賽,比的就是這個。但是,這個對工程,用處好像并不是很大。領(lǐng)域奠基比如約翰*卡馬克,他創(chuàng)造出了現(xiàn)代計算機(jī)圖形高效渲染的方法論。不論如果沒有他,后面會不會有人發(fā)明,他就是第一個發(fā)明了。1999年,卡馬克登上了美國時代雜志評選出來的科技領(lǐng)域50大影響力人物榜單,并且名列第10位。但是,類似的殿堂級位置,沒有幾個,不夠大家分,沒我們的事兒。理論研究八十年代李開復(fù)博士堅持采用隱含馬爾可夫模型的框架,成功地開發(fā)了世界上第一個大詞匯量連續(xù)語音識別系統(tǒng)Sphinx。我輩工程師,好像擅長這個的很少。產(chǎn)品成功小龍哥是標(biāo)桿。最佳實(shí)踐這個是大家都可以做到,按照上面架構(gòu)師的定義。在這條路上走得好,就能為任何公司組建技術(shù)團(tuán)隊(duì),組織建設(shè)高質(zhì)量的系統(tǒng)。從上面的討論中,可以看出,我們普通工程師的進(jìn)化之路,就是不斷打磨最佳實(shí)踐方法論、落地細(xì)節(jié)。代碼變壞的根源在討論什么代碼是好代碼之前,我們先討論什么是不好的。計算機(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è)計得不好,第一個使用接口的人,沒有類似上面這個函數(shù)的代碼,自己實(shí)現(xiàn)了一個嵌入邏輯代碼的填寫請求結(jié)構(gòu)結(jié)構(gòu)體的代碼,一開始,挺好的。但當(dāng)有第二個人,第三個人干了類似的事情,我們將無法再重構(gòu)這個協(xié)議,必須做到麻煩的向前兼容。而且每個同學(xué),都要理解一遍上面這個協(xié)議怎么填,理解有問題,就觸發(fā)bug?;蛘撸绻硞€錯誤的理解,普遍存在,我們就得找到所有這些重復(fù)的片段,都修改一遍。當(dāng)你要讀一個數(shù)據(jù),發(fā)現(xiàn)兩個地方有,不知道該選擇哪個。當(dāng)你要實(shí)現(xiàn)一個功能,發(fā)現(xiàn)兩個rpc接口、兩個函數(shù)能做到,你不知道選哪一個。你有面臨過這樣的'人生難題'么?其實(shí)怎么選并不重要了,你寫的這個代碼已經(jīng)在走向shit的道路上邁出了堅實(shí)的一步。但是,Alittlecopyingisbetterthanalittledependency。這里提一嘴,不展開。這里,我必須額外說一句。大家使用trpc。感覺自己被鼓勵'每個服務(wù)搞一個git'。那,你這個服務(wù)里訪問db的代碼,rpc的代碼,各種可以復(fù)用的代碼,是用的大家都復(fù)用的git下的代碼么?每次都重復(fù)寫一遍,db字段細(xì)節(jié)改了,每個使用過db的server對應(yīng)的git都改一遍?這個通用git已經(jīng)寫好的接口應(yīng)該不知道哪些git下的代碼因?yàn)樽约翰幌蚯凹嫒莸男薷亩肋h(yuǎn)放棄了向前不兼容的修改?早期有效的決策不再有效很多時候,我們第一版代碼寫出來,是沒有太大的問題的。比如,下面這個代碼//

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)在看,這個代碼挺好的,長度沒超過80行,邏輯比價清晰。但是當(dāng)isMerge這里判斷邏輯,如果加入更多的邏輯,把局部行數(shù)撐到50行以上,這個函數(shù),味道就壞了。出現(xiàn)兩個問題:1)函數(shù)內(nèi)代碼不在一個邏輯層次上,閱讀代碼,本來在閱讀著頂層邏輯,突然就掉入了長達(dá)50行的isMerge的邏輯處理細(xì)節(jié),還沒看完,讀者已經(jīng)忘了前面的代碼講了什么,需要來回看,挑戰(zhàn)自己大腦的cache尺寸。2)代碼有問題后,再新加代碼的同學(xué),是改還是不改前人寫好的代碼呢?出bug誰來背?這是一個靈魂拷問。過早的優(yōu)化這個大家聽了很多了,這里不贅述。對合理性沒有苛求'兩種寫法都o(jì)k,你隨便挑一種吧','我這樣也沒什么吧',這是我經(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)造的時候,好像都行。這個時候,很多同學(xué)態(tài)度就變得不堅決。實(shí)際上,這里必須是defer的。

i.l.Lock()

defer

i.l.Unlock()

var

err

error

ip,

err

=

getNetIP(cardName)

if

err

!=

nil

{

return

""

}

i.m[cardName]

=

ip

return

ip這樣的修改,是極有可能發(fā)生的,它還是要變成defer,那,為什么不一開始就是defer,進(jìn)入最合理的狀態(tài)?不一開始就進(jìn)入最合理的狀態(tài),在后續(xù)協(xié)作中,其他同學(xué)很可能犯錯!總是面向?qū)ο?總喜歡封裝我是軟件工程科班出身。學(xué)的第一門編程語言是c++。教材是這本

。當(dāng)時自己讀完教材,初入程序設(shè)計之門,對于里面講的'封裝',驚為天人,多么美妙的設(shè)計啊,面向?qū)ο?,多么智慧的設(shè)計啊。但是,這些年來,我看到了大牛'云風(fēng)'對于'畢業(yè)生使用mysqlapi就喜歡搞個class封裝再用'的嘲諷;看到了各種莫名其妙的class定義;體會到了經(jīng)常要去看一個莫名其妙的繼承樹,必須要把整個繼承樹整體讀明白才能確認(rèn)一個細(xì)小的邏輯分支;多次體會到了我需要辛苦地壓抑住自己的抵觸情緒,去細(xì)度一個自作聰明的被封裝的代碼,確認(rèn)我的bug。除了UI類場景,我認(rèn)為少用繼承、多用組合。搜索公眾號后端架構(gòu)師后臺回復(fù)“面試”,獲取一份驚喜禮包。template<class

_PKG_TYPE>

class

CSuperAction

:

public

CSuperActionBase

{

public:

typedef

_PKG_TYPE

pkg_type;

typedef

CSuperAction

this_type;

...

}這是sspp的代碼。CSuperAction和CSuperActionBase,一會兒super,一會兒base,Super和SuperBase是在怎樣的兩個抽象層次上,不通讀代碼,沒人能讀明白。我想確認(rèn)任何細(xì)節(jié),都要把多個層次的代碼都通讀了,有什么封裝性可言?好,你說是沒有把classname取得好。那,問題是,你能取得好么?一個剛?cè)肼毜腡1.2的同學(xué)能把classname、class樹設(shè)計得好么?即使是對簡單的業(yè)務(wù)模型,也需要無數(shù)次'壞'的對象抽象實(shí)踐,才能培養(yǎng)出一個具有合格的class抽象能力的同學(xué),這對于大型卻松散的團(tuán)隊(duì)協(xié)作,不是破壞性的?已經(jīng)有了一套繼承樹,想要添加功能就只能在這個繼承樹里添加,以前的繼承樹不再適合新的需求,這個繼承樹上所有的class,以及使用它們的地方,你都去改?不,是個正常人都會放棄,開始堆屎山。封裝,就是我可以不關(guān)心實(shí)現(xiàn)。但是,做一個穩(wěn)定的系統(tǒng),每一層設(shè)計都可能出問題。abi,總有合適的用法和不合適的用法,真的存在我們能完全不關(guān)心封裝的部分是怎么實(shí)現(xiàn)的?不,你不能。bug和性能問題,常常就出現(xiàn)在,你用了錯誤的用法去使用一個封裝好的函數(shù)。即使是android、ios的api,golang、java現(xiàn)成的api,我們常常都要去探究實(shí)現(xiàn),才能把a(bǔ)pi用好。那,我們是不是該一上來,就做一個透明性很強(qiáng)的函數(shù),才更為合理?使用者想知道細(xì)節(jié),進(jìn)來吧,我的實(shí)現(xiàn)很易讀,你看看就明白,使用時不會迷路!對于邏輯復(fù)雜的函數(shù),我們還要強(qiáng)調(diào)函數(shù)內(nèi)部工作方式'可以讓讀者在大腦里想象呈現(xiàn)完整過程'的可現(xiàn)性,讓使用者輕松讀懂,有把握,使用時,不迷路!根本沒有設(shè)計這個最可怕,所有需求,上手就是一頓擼,'設(shè)計是什么東西?我一個文件5w行,一個函數(shù)5k行,干不完需求?'從第一行代碼開始,就是無設(shè)計的,隨意地踩著滿地的泥坑,對于旁人的眼光沒有感覺,一個人獨(dú)舞,產(chǎn)出的代碼,完成了需求,毀滅了接手自己代碼的人。這個就不舉例了,每個同學(xué)應(yīng)該都能在自己的項(xiàng)目類發(fā)現(xiàn)這種代碼。必須形而上的思考常常,同學(xué)們聽演講,公開課,就喜歡聽一些細(xì)枝末節(jié)的'干活'。這沒有問題。但是,你干了幾年活,學(xué)習(xí)了多少干貨知識點(diǎn)?構(gòu)建起自己的技術(shù)思考'面',進(jìn)入立體的'工程思維',把技術(shù)細(xì)節(jié)和系統(tǒng)要滿足的需求在思考上連接起來了么?當(dāng)聽一個需求的時候,你能思考到自己的codepackage該怎么組織,函數(shù)該怎么組織了么?那,技術(shù)點(diǎn)要怎么和需求連接起來呢?答案很簡單,你需要在時間里總結(jié),總結(jié)出一些明確的原則、思維過程。思考怎么去總結(jié),特別像是在思考哲學(xué)問題。從一些瑣碎的細(xì)節(jié)中,由具體情況上升到一些原則、公理。同時,大家在接受原則時,不應(yīng)該是接受和記住原則本身,而應(yīng)該是結(jié)構(gòu)原則,讓這個原則在自己這里重新推理一遍,自己完全掌握這個原則的適用范圍。再進(jìn)一步具體地說,對于工程最佳實(shí)踐的形而上的思考過程,就是:把工程實(shí)踐中遇到的問題,從問題類型和解法類型,兩個角度去歸類,總結(jié)出一些有限適用的原則,就從點(diǎn)到了面。把諸多總結(jié)出的原則,組合應(yīng)用到自己的項(xiàng)目代碼中,就是把多個面結(jié)合起來構(gòu)建了一套立體的最佳實(shí)踐的方案。當(dāng)你這套方案能適應(yīng)30w+行代碼的項(xiàng)目,超過30人的項(xiàng)目,你就架構(gòu)師入門了!當(dāng)你這個項(xiàng)目,是多端,多語言,代碼量超過300w行,參與人數(shù)超過300人,代碼質(zhì)量依然很高,代碼依然在高效地自我迭代,每天消除掉過時的代碼,填充高質(zhì)量的替換舊代碼和新生的代碼。恭喜你,你已經(jīng)是一個很高級的架構(gòu)師了!再進(jìn)一步,你對某個業(yè)務(wù)模型有獨(dú)到或者全面的理解,構(gòu)建了一套行業(yè)第一的解決方案,結(jié)合剛才高質(zhì)量實(shí)現(xiàn)的能力,實(shí)現(xiàn)了這么一個項(xiàng)目。沒啥好說的,你已經(jīng)是專家工程師了。級別再高,我就不了解了,不在這里討論。那么,我們要重頭開始積累思考和總結(jié)?不,有一本書叫做《unix編程藝術(shù)》,我在不同的時期分別讀了3遍,等一會,我講一些里面提到的,我覺得在騰訊尤其值得拿出來說的原則。這些原則,正好就能作為codereview時大家判定代碼質(zhì)量的準(zhǔn)繩。但,在那之前,我得講一下另外一個很重要的話題,模型設(shè)計。model設(shè)計沒讀過oauth2.0RFC,就去設(shè)計第三方授權(quán)登陸的人,終歸還要再發(fā)明一個撇腳的oauth。2012年我剛畢業(yè),我和一個去了廣州聯(lián)通公司的華南理工畢業(yè)生聊天。當(dāng)時他說他工作很不開心,因?yàn)楣ぷ骼锊唤?jīng)常寫代碼,而且認(rèn)為自己有ACM競賽金牌級的算法熟練度+對CPP代碼的熟悉,寫下一個個指針操作內(nèi)存,什么程序?qū)懖怀鰜?,什么事情做不好。?dāng)時我覺得,挺有道理,編程工具在手,我什么事情做不了?現(xiàn)在,我會告訴他,復(fù)雜如linux操作系統(tǒng)、Chromium引擎、windowsoffice,你做不了。原因是,他根本沒進(jìn)入軟件工程的工程世界。不是會搬磚就能修出港珠澳大橋。但是,這么回答并不好,舉證用的論據(jù)離我們太遙遠(yuǎn)了。見微知著。我現(xiàn)在會回答,你做不了,簡單如一個權(quán)限系統(tǒng),你知道怎么做么?堆積一堆邏輯層次一維展開的ifelse?簡單如一個共享文件管理,你知道怎么做么?堆積一堆邏輯層次一維展開的ifelse?你聯(lián)通有上萬臺服務(wù)器,你要怎么寫一個管理平臺?堆積一堆邏輯層次一維展開的ifelse?上來就是干,能實(shí)現(xiàn)上面提到的三個看似簡單的需求?想一想,亞馬遜、阿里云折騰了多少年,最后才找到了容器+Kubernetes的大殺器。這里,需要谷歌多少年在BORG系統(tǒng)上的實(shí)踐,提出了優(yōu)秀的服務(wù)編排領(lǐng)域模型。權(quán)限領(lǐng)域,有RBAC、DAC、MAC等等模型,到了業(yè)務(wù),又會有細(xì)節(jié)的不同。如DomainDrivenDesign說的,沒有良好的領(lǐng)域思考和模型抽象,邏輯復(fù)雜度就是n^2指數(shù)級的,你得寫多少ifelse,得思考多少可能的if路徑,來cover所有的不合符預(yù)期的情況。你必須要有Domain思考探索、model拆解/抽象/構(gòu)建的能力。有人問過我,要怎么有效地獲得這個能力?這個問題我沒能回答,就像是在問我,怎么才能獲得MIT博士的學(xué)術(shù)能力?我無法回答。唯一回答就是,進(jìn)入某個領(lǐng)域,就是首先去看前人的思考,站在前人的肩膀上,再用上自己的通識能力,去進(jìn)一步思考。至于怎么建立好的通識思考能力,可能得去常青藤讀個書吧:)或者,就在工程實(shí)踐中思考和鍛煉自己的這個能力!同時,基于model設(shè)計的代碼,能更好地適應(yīng)產(chǎn)品經(jīng)理不斷變更的需求。比如說,一個calendar(日歷)應(yīng)用,簡單來想,不要太簡單!以'userid_date'為key記錄一個用戶的每日安排不就完成了么?只往前走一步,設(shè)計了一個任務(wù),上限分發(fā)給100w個人,創(chuàng)建這么一個任務(wù),是往100w個人下面添加一條記錄?你得改掉之前的設(shè)計,換db。再往前走一步,要拉出某個用戶和某個人一起要參與的所有事務(wù),是把兩個人的所有任務(wù)來做join?好像還行。如果是和100個人一起參與的所有任務(wù)呢?100個人的任務(wù)來join?不現(xiàn)實(shí)了吧。好,你引入一個群組id,那么,你最開始的'userid_date'為key的設(shè)計,是不是又要修改和做數(shù)據(jù)遷移了?經(jīng)常來一個需求,你就得把系統(tǒng)推翻重來,或者根本就只能拒絕用戶的需求,這樣的戰(zhàn)斗力,還好意思叫自己工程師?你一開始就應(yīng)該思考自己面對的業(yè)務(wù)領(lǐng)域,思考自己的日歷應(yīng)用可能的模型邊界,把可能要做的能力都拿進(jìn)來思考,構(gòu)建一個model,設(shè)計一套通用的store層接口,基于通用接口的邏輯代碼。當(dāng)產(chǎn)品不斷發(fā)展,就是不停往模型里填內(nèi)容,而不是推翻重來。這,思考模型邊界,構(gòu)建模型細(xì)節(jié),就是兩個很重要的能力,也是絕大多數(shù)騰訊產(chǎn)品經(jīng)理不具備的能力,你得具備,對整個團(tuán)隊(duì)都是極其有益的。你面對產(chǎn)品經(jīng)理時,就聽取他們出于對用戶體驗(yàn)負(fù)責(zé)思考出的需求點(diǎn),到你自己這里,用一個完整的模型去涵蓋這些零碎的點(diǎn)。model設(shè)計,是形而上思考中的一個方面,一個特別重要的方面。接下來,我們來抄襲抄襲unix操作系統(tǒng)構(gòu)建的實(shí)踐為我們提出的前人實(shí)踐經(jīng)驗(yàn)和'公理'總結(jié)。在自己的coding/codereview中,站在巨人的肩膀上去思考。不重復(fù)地發(fā)現(xiàn)經(jīng)典力學(xué),而是往相對論挺進(jìn)。UNIX設(shè)計哲學(xué)不懂Unix的人注定最終還要重復(fù)發(fā)明一個撇腳的Unix。--HenrySpenncer,1987.11下面這一段話太經(jīng)典,我必須要摘抄一遍(自《UNIX編程藝術(shù)》):“工程和設(shè)計的每個分支都有自己的技術(shù)文化。在大多數(shù)工程領(lǐng)域中,就一個專業(yè)人員的素養(yǎng)組成來說,有些不成文的行業(yè)素養(yǎng)具有與標(biāo)準(zhǔn)手冊及教科書同等重要的地位(并且隨著專業(yè)人員經(jīng)驗(yàn)的日積月累,這些經(jīng)驗(yàn)常常會比書本更重要)。資深工程師們在工作中會積累大量的隱性知識,他們用類似禪宗'教外別傳'的方式,通過言傳身教傳授給后輩。軟件工程算是此規(guī)則的一個例外:技術(shù)變革如此之快,軟件環(huán)境日新月異,軟件技術(shù)文化暫如朝露。然而,例外之中也有例外。確有極少數(shù)軟件技術(shù)被證明經(jīng)久耐用,足以演進(jìn)為強(qiáng)勢的技術(shù)文化、有鮮明特色的藝術(shù)和世代相傳的設(shè)計哲學(xué)?!敖酉聛?,我用我的理解,講解一下幾個我們常常做不到的原則。KeepItSimpleStuped!KISS原則,大家應(yīng)該是如雷貫耳了。但是,你真的在遵守?什么是Simple?簡單?golang語言主要設(shè)計者之一的RobPike說'大道至簡',這個'簡'和簡單是一個意思么?首先,簡單不是面對一個問題,我們印入眼簾第一映像的解法為簡單。我說一句,感受一下。"把一個事情做出來容易,把事情用最簡單有效的方法做出來,是一個很難的事情。"比如,做一個三方授權(quán),oauth2.0很簡單,所有概念和細(xì)節(jié)都是緊湊、完備、易用的。你覺得要設(shè)計到oauth2.0這個效果很容易么?要做到簡單,就要對自己處理的問題有全面的了解,然后需要不斷積累思考,才能做到從各個角度和層級去認(rèn)識這個問題,打磨出一個通俗、緊湊、完備的設(shè)計,就像ios的交互設(shè)計。簡單不是容易做到的,需要大家在不斷的時間和codereview過程中去積累思考,pk中觸發(fā)思考,交流中總結(jié)思考,才能做得愈發(fā)地好,接近'大道至簡'。搜索公眾號頂級架構(gòu)師后臺回復(fù)“架構(gòu)”,獲取一份驚喜禮包。兩張經(jīng)典的模型圖,簡單又全面,感受一下,沒看懂,可以立即自行g(shù)oogle學(xué)習(xí)一下:RBAC:logging:原則3組合原則:設(shè)計時考慮拼接組合關(guān)于OOP,關(guān)于繼承,我前面已經(jīng)說過了。那我們怎么組織自己的模塊?對,用組合的方式來達(dá)到。linux操作系統(tǒng)離我們這么近,它是怎么架構(gòu)起來的?往小里說,我們一個串聯(lián)一個業(yè)務(wù)請求的數(shù)據(jù)集合,如果使用BaseSession,XXXSessioninheritBaseSession的設(shè)計,其實(shí),這個繼承樹,很難適應(yīng)層出不窮的變化。但是如果使用組合,就可以拆解出UserSignature等等各種可能需要的部件,在需要的時候組合使用,不斷添加新的部件而沒有對老的繼承樹的記憶這個心智負(fù)擔(dān)。使用組合,其實(shí)就是要讓你明確清楚自己現(xiàn)在所擁有的是哪個部件。如果部件過于多,其實(shí)完成組合最終成品這個步驟,就會有較高的心智負(fù)擔(dān),每個部件展開來,琳瑯滿目,眼花繚亂。比如QT這個通用UI框架,看它的Class列表,有1000多個。如果不用繼承樹把它組織起來,平鋪展開,組合出一個頁面,將會變得心智負(fù)擔(dān)高到無法承受。OOP在'需要無數(shù)元素同時展現(xiàn)出來'這種復(fù)雜度極高的場景,有效的控制了復(fù)雜度。'那么,古爾丹,代價是什么呢?'代價就是,一開始做出這個自上而下的設(shè)計,牽一發(fā)而動全身,每次調(diào)整都變得異常困難。實(shí)際項(xiàng)目中,各種職業(yè)級別不同的同學(xué)一起協(xié)作修改一個server的代碼,就會出現(xiàn),職級低的同學(xué)改哪里都改不對,根本沒能力進(jìn)行修改,高級別的同學(xué)能修改對,也不愿意大規(guī)模修改,整個項(xiàng)目變得愈發(fā)不合理。對整個繼承樹沒有完全認(rèn)識的同學(xué)都沒有資格進(jìn)行任何一個對繼承樹有調(diào)整的修改,協(xié)作變得寸步難行。代碼的修改,都變成了依賴一個高級架構(gòu)師高強(qiáng)度監(jiān)控繼承體系的變化,低級別同學(xué)們束手束腳的結(jié)果。組合,就很好的解決了這個問題,把問題不斷細(xì)分,每個同學(xué)都可以很好地攻克自己需要攻克的點(diǎn),實(shí)現(xiàn)一個package。產(chǎn)品邏輯代碼,只需要去組合各個package,就能達(dá)到效果。這是golang標(biāo)準(zhǔn)庫里httprequest的定義,它就是Http請求所有特性集合出來的結(jié)果。其中通用/異變/多種實(shí)現(xiàn)的部分,通過duckinterface抽象,比如Bodyio.ReadCloser。你想知道哪些細(xì)節(jié),就從組合成request的部件入手,要修改,只需要修改對應(yīng)部件。[這段代碼后,對比.NET的HTTP基于OOP的抽象]//ARequestrepresentsanHTTPrequestreceivedbyaserver

//ortobesentbyaclient.

//

//Thefieldsemanticsdifferslightlybetweenclientandserver

//usage.Inadditiontothenotesonthefieldsbelow,seethe

//documentationforRequest.WriteandRoundTripper.

typeRequeststruct{

//MethodspecifiestheHTTPmethod(GET,POST,PUT,etc.).

//Forclientrequests,anemptystringmeansGET.

//

//Go'sHTTPclientdoesnotsupportsendingarequestwith

//theCONNECTmethod.SeethedocumentationonTransportfor

//details.

Methodstring

//URLspecifieseithertheURIbeingrequested(forserver

//requests)ortheURLtoaccess(forclientrequests).

//

//Forserverrequests,theURLisparsedfromtheURI

//suppliedontheRequest-LineasstoredinRequestURI.For

//mostrequests,fieldsotherthanPathandRawQuerywillbe

//empty.(SeeRFC7230,Section5.3)

//

//Forclientrequests,theURL'sHostspecifiestheserverto

//connectto,whiletheRequest'sHostfieldoptionally

//specifiestheHostheadervaluetosendintheHTTP

//request.

URL*url.URL

//Theprotocolversionforincomingserverrequests.

//

//Forclientrequests,thesefieldsareignored.TheHTTP

//clientcodealwaysuseseitherHTTP/1.1orHTTP/2.

//SeethedocsonTransportfordetails.

Protostring//"HTTP/1.0"

ProtoMajorint//1

ProtoMinorint//0

//Headercontainstherequestheaderfieldseitherreceived

//bytheserverortobesentbytheclient.

//

//Ifaserverreceivedarequestwithheaderlines,

//

// Host:

// 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"},

// }

//

//Forincomingrequests,theHostheaderispromotedtothe

//Request.HostfieldandremovedfromtheHeadermap.

//

//HTTPdefinesthatheadernamesarecase-insensitive.The

//requestparserimplementsthisbyusingCanonicalHeaderKey,

//makingthefirstcharacterandanycharactersfollowinga

//hyphenuppercaseandtherestlowercase.

//

//Forclientrequests,certainheaderssuchasContent-Length

//andConnectionareautomaticallywrittenwhenneededand

//valuesinHeadermaybeignored.Seethedocumentation

//fortheRequest.Writemethod.

HeaderHeader

//Bodyistherequest'sbody.

//

//Forclientrequests,anilbodymeanstherequesthasno

//body,suchasaGETrequest.TheHTTPClient'sTransport

//isresponsibleforcallingtheClosemethod.

//

//Forserverrequests,theRequestBodyisalwaysnon-nil

//butwillreturnEOFimmediatelywhennobodyispresent.

//TheServerwillclosetherequestbody.TheServeHTTP

//Handlerdoesnotneedto.

Bodyio.ReadCloser

//GetBodydefinesanoptionalfunctoreturnanewcopyof

//Body.Itisusedforclientrequestswhenaredirectrequires

//readingthebodymorethanonce.UseofGetBodystill

//requiressettingBody.

//

//Forserverrequests,itisunused.

GetBodyfunc()(io.ReadCloser,error)

//ContentLengthrecordsthelengthoftheassociatedcontent.

//Thevalue-1indicatesthatthelengthisunknown.

//Values>=0indicatethatthegivennumberofbytesmay

//bereadfromBody.

//

//Forclientrequests,avalueof0withanon-nilBodyis

//alsotreatedasunknown.

ContentLengthint64

//TransferEncodingliststhetransferencodingsfromoutermostto

//innermost.Anemptylistdenotesthe"identity"encoding.

//TransferEncodingcanusuallybeignored;chunkedencodingis

//automaticallyaddedandremovedasnecessarywhensendingand

//receivingrequests.

TransferEncoding[]string

//Closeindicateswhethertoclosetheconnectionafter

//replyingtothisrequest(forservers)oraftersendingthis

//requestandreadingitsresponse(forclients).

//

//Forserverrequests,theHTTPserverhandlesthisautomatically

//andthisfieldisnotneededbyHandlers.

//

//Forclientrequests,settingthisfieldpreventsre-useof

//TCPconnectionsbetweenrequeststothesamehosts,asif

//Transport.DisableKeepAliveswereset.

Closebool

//Forserverrequests,Hostspecifiesthehostonwhichthe

//URLissought.ForHTTP/1(perRFC7230,section5.4),this

//iseitherthevalueofthe"Host"headerorthehostname

//givenintheURLitself.ForHTTP/2,itisthevalueofthe

//":authority"pseudo-headerfield.

//Itmaybeoftheform"host:port".Forinternationaldomain

//names,HostmaybeinPunycodeorUnicodeform.Use

///x/net/idnatoconvertittoeitherformatif

//needed.

//TopreventDNSrebindingattacks,serverHandlersshould

//validatethattheHostheaderhasavalueforwhichthe

//Handlerconsidersitselfauthoritative.Theincluded

//ServeMuxsupportspatternsregisteredtoparticularhost

//namesandthusprotectsitsregisteredHandlers.

//

//Forclientrequests,HostoptionallyoverridestheHost

//headertosend.Ifempty,theRequest.Writemethoduses

//thevalueofURL.Host.Hostmaycontainaninternational

//domainname.

Hoststring

//Formcontainstheparsedformdata,includingboththeURL

//field'squeryparametersandthePATCH,POST,orPUTformdata.

//ThisfieldisonlyavailableafterParseFormiscalled.

//TheHTTPclientignoresFormandusesBodyinstead.

Formurl.Values

//PostFormcontainstheparsedformdatafromPATCH,POST

//orPUTbodyparameters.

//

//ThisfieldisonlyavailableafterParseFormiscalled.

//TheHTTPclientignoresPostFormandusesBodyinstead.

PostFormurl.Values

//MultipartFormistheparsedmultipartform,includingfileuploads.

//ThisfieldisonlyavailableafterParseMultipartFormiscalled.

//TheHTTPclientignoresMultipartFormandusesBodyinstead.

MultipartForm*multipart.Form

//Trailerspecifiesadditionalheadersthataresentaftertherequest

//body.

//

//Forserverrequests,theTrailermapinitiallycontainsonlythe

//trailerkeys,withnilvalues.(Theclientdeclareswhichtrailersit

//willlatersend.)WhilethehandlerisreadingfromBody,itmust

//notreferenceTrailer.AfterreadingfromBodyreturnsEOF,Trailer

//canbereadagainandwillcontainnon-nilvalues,iftheyweresent

//bytheclient.

//

//Forclientrequests,Trailermustbeinitializedtoamapcontaining

//thetrailerkeystolatersend.Thevaluesmaybenilortheirfinal

//values.TheContentLengthmustbe0or-1,tosendachunkedrequest.

//AftertheHTTPrequestissentthemapvaluescanbeupdatedwhile

//therequestbodyisread.OncethebodyreturnsEOF,thecallermust

//notmutateTrailer.

//

//FewHTTPclients,servers,orproxiessupportHTTPtrailers.

TrailerHeader

//RemoteAddrallowsHTTPserversandothersoftwaretorecord

//thenetworkaddressthatsenttherequest,usuallyfor

//logging.ThisfieldisnotfilledinbyReadRequestand

//hasnodefinedformat.TheHTTPserverinthispackage

//setsRemoteAddrtoan"IP:port"addressbeforeinvokinga

//handler.

//ThisfieldisignoredbytheHTTPclient.

RemoteAddrstring

//RequestURIistheunmodifiedrequest-targetofthe

//Request-Line(RFC7230,Section3.1.1)assentbytheclient

//toaserver.UsuallytheURLfieldshouldbeusedinstead.

//ItisanerrortosetthisfieldinanHTTPclientrequest.

RequestURIstring

//TLSallowsHTTPserversandothersoftwaretorecord

//informationabouttheTLSconnectiononwhichtherequest

//wasreceived.ThisfieldisnotfilledinbyReadRequest.

//TheHTTPserverinthispackagesetsthefieldfor

//TLS-enabledconnectionsbeforeinvokingahandler;

//otherwiseitleavesthefieldnil.

//ThisfieldisignoredbytheHTTPclient.

TLS*tls.ConnectionState

//Cancelisanoptionalchannelwhoseclosureindicatesthattheclient

//requestshouldberegardedascanceled.Notallimplementationsof

//RoundTrippermaysupportCancel.

//

//Forserverrequests,thisfieldisnotapplicable.

//

//Deprecated:SettheRequest'scontextwithNewRequestWithContext

//instead.IfaRequest'sCancelfieldandcontextareboth

//set,itisundefinedwhetherCancelisrespected.

Cancel<-chanstruct{}

//Responseistheredirectresponsewhichcausedthisrequest

//tobecreated.Thisfieldisonlypopulatedduringclient

//redirects.

Response*Response

//ctxiseithertheclientorservercontext.Itshouldonly

//bemodifiedviacopyingthewholeRequestusingWithContext.

//ItisunexportedtopreventpeoplefromusingContextwrong

//andmutatingthecontextsheldbycallersofthesamerequest.

ctxcontext.Context

}看看.NET里對于web服務(wù)的抽象,僅僅看到末端,不去看完整個繼承樹的完整圖景,我根本無法知道我關(guān)心的某個細(xì)節(jié)在什么位置。進(jìn)而,我要往整個http服務(wù)體系里修改任何功能,都無法拋開對整體完整設(shè)計的理解和熟悉,還極容易沒有知覺地破壞者整體的設(shè)計。說到組合,還有一個關(guān)系很緊密的詞,叫插件化。大家都用vscode用得很開心,它比visualstudio成功在哪里?如果vscode通過添加一堆插件達(dá)到visualstudio具備的能力,那么它將變成另一個和visualstudio差不多的東西,叫做vsstudio吧。大家應(yīng)該發(fā)現(xiàn)問題了,我們很多時候其實(shí)并不需要visualstudio的大多數(shù)功能,而且希望靈活定制化一些比較小眾的能力,用一些小眾的插件。甚至,我們希望選擇不同實(shí)現(xiàn)的同類型插件。這就是組合的力量,各種不同的組合,它簡單,卻又滿足了各種需求,靈活多變,要實(shí)現(xiàn)一個插件,不需要事先掌握一個龐大的體系。體現(xiàn)在代碼上,也是一樣的道理。至少后端開發(fā)領(lǐng)域,組合,比OOP,'香'很多。原則6吝嗇原則:除非確無它法,不要編寫龐大的程序可能有些同學(xué)會覺得,把程序?qū)懙谬嫶笠恍┎藕媚玫贸鍪秩ピuT11、T12。leader們一看評審方案就容易覺得:很大,很好,很全面。但是,我們真的需要寫這么大的程序么?我又要說了"那么,古爾丹,代價是什么呢?"。代價是代碼越多,越難維護(hù),難調(diào)整。C語言之父KenThompson說"刪除一行代碼,給我?guī)淼某删透幸忍砑右恍幸?。我們對于代碼,要吝嗇。能把系統(tǒng)做小,就不要做大。騰訊不乏200w+行的客戶端,很大,很牛。但是,同學(xué)們自問,現(xiàn)在還調(diào)整得動架構(gòu)么。手Q的同學(xué)們,看看自己代碼,曾經(jīng)嘆息過么。能小做的事情就小做,尋求通用化,通過duckinterface(甚至多進(jìn)程,用于隔離能力的多線程)把模塊、能力隔離開,時刻想著刪減代碼量,才能保持代碼的可維護(hù)性和面對未來的需求、架構(gòu),調(diào)整自身的活力??蛻舳舜a,UI渲染模塊可以復(fù)雜吊炸天,非UI部分應(yīng)該追求最簡單,能力接口化,可替換、重組合能力強(qiáng)。落地到大家的代碼,review時,就應(yīng)該最關(guān)注核心struct定義,構(gòu)建起一個完備的模型,核心interface,明確抽象model對外部的依賴,明確抽象model對外提供的能力。其他代碼,就是要用最簡單、平平無奇的代碼實(shí)現(xiàn)模型內(nèi)部細(xì)節(jié)。原則7透明性原則:設(shè)計要可見,以便審查和調(diào)試首先,定義一下,什么是透明性和可顯性。"如果沒有陰暗的角落和隱藏的深度,軟件系統(tǒng)就是透明的。透明性是一種被動的品質(zhì)。如果實(shí)際上能預(yù)測到程序行為的全部或大部分情況,并能建立簡單的心理模型,這個程序就是透明的,因?yàn)榭梢钥赐笝C(jī)器究竟在干什么。如果軟件系統(tǒng)所包含的功能是為了幫助人們對軟件建立正確的'做什么、怎么做'的心理模型而設(shè)計,這個軟件系統(tǒng)就是可顯的。因此,舉例來說,對用戶而言,良好的文檔有助于提高可顯性;對程序員而言,良好的變量和函數(shù)名有助于提高可顯性??娠@性是一種主動品質(zhì)。在軟件中要達(dá)到這一點(diǎn),僅僅做到不晦澀是不夠的,還必須要盡力做到有幫助。"我們要寫好程序,減少bug,就要增強(qiáng)自己對代碼的控制力。你始終做到,理解自己調(diào)用的函數(shù)/復(fù)用的代碼大概是怎么實(shí)現(xiàn)的。不然,你可能就會在單線程狀態(tài)機(jī)的server里調(diào)用有IO阻塞的函數(shù),讓自己的server吞吐量直接掉到底。進(jìn)而,為了保證大家能對自己代碼能做到有控制力,所有人寫的函數(shù),就必須具備很高的透明性。而不是寫一些看了一陣看不明白的函數(shù)/代碼,結(jié)果被迫使用你代碼的人,直接放棄了對掌控力的追取,甚至放棄復(fù)用你的代碼,另起爐灶,走向了'制造重復(fù)代碼'的深淵。透明性其實(shí)相對容易做到的,大家有意識地鍛煉一兩個月,就能做得很好。可顯性就不容易了。有一個現(xiàn)象是,你寫的每一個函數(shù)都不超過80行,每一行我都能看懂,但是你層層調(diào)用,很多函數(shù)調(diào)用,組合起來怎么就實(shí)現(xiàn)了某個功能,看兩遍,還是看不懂。第三遍可能才能大概看懂。大概看懂了,但太復(fù)雜,很難在大腦里構(gòu)建起你實(shí)現(xiàn)這個功能的整體流程。結(jié)果就是,閱讀者根本做不到對你的代碼有好的掌控力。搜索公眾號后端架構(gòu)師后臺回復(fù)“架構(gòu)整潔”,獲取一份驚喜禮包。可顯性的標(biāo)準(zhǔn)很簡單,大家看一段代碼,懂不懂,一下就明白了。但是,如何做好可顯性?那就是要追求合理的函數(shù)分組,合理的函數(shù)上下級層次,同一層次的代碼才會出現(xiàn)在同一個函數(shù)里,追求通俗易懂的函數(shù)分組分層方式,是通往可顯性的道路。當(dāng)然,復(fù)雜如linux操作系統(tǒng),office文檔,問題本身就很復(fù)雜,拆解、分層、組合得再合理,都難建立心理模型。這個時候,就需要完備的文檔了。完備的文檔還需要出現(xiàn)在離代碼最近的地方,讓人'知道這里復(fù)雜的邏輯有文檔',而不是其實(shí)文檔,但是閱讀者不知道。再看看上面golang標(biāo)準(zhǔn)庫里的http.Request,感受到它在可顯性上的努力了么?對,就去學(xué)它。原則10通俗原則:接口設(shè)計避免標(biāo)新立異設(shè)計程序過于標(biāo)新立異的話,可能會提升別人理解的難度。一般,我們這么定義一個'點(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é)作成本。上面的例子常見,但還不是最小立異原則最想說明的問題。想想一下,一個程序里,你把用'+'這個符號表示數(shù)組添加元素,而不是數(shù)學(xué)'加','result:=1+2'-->'result=[]int{1,2}'而不是'result=3',那么,你這個標(biāo)新立異,對程序的破壞性,簡直無法想象。"最小立異原則的另一面是避免表象想死而實(shí)際卻略有不同。這會極端危險,因?yàn)楸硐笙嗨仆鶎?dǎo)致人們產(chǎn)生錯誤的假定。所以最好讓不同事物有明顯區(qū)別,而不要看起來幾乎一模一樣。"--HenrySpencer。你實(shí)現(xiàn)一個db.Add()函數(shù)卻做著db.AddOrUpdate()的操作,有人使用了你的接口,錯誤地把數(shù)據(jù)覆蓋了。原則11緘默原則:如果一個程序沒什么好說的,就沉默這個原則,應(yīng)該是大家最經(jīng)常破壞的原則之一。一段簡短的代碼里插入了各種'log("cmdxxxenter")','log("reqdata"+req.String())',非常害怕自己信息打印得不夠。害怕自己不知道程序執(zhí)行成功了,總要最后'log("success")'。但是,我問一下大家,你們真的耐心看過別人寫的代碼打的一堆日志么?不是自己需要哪個,就在一堆日志里,再打印一個日志出來一個帶有特殊標(biāo)記的日志'log("this_is_my_log_"+xxxxx)'?結(jié)果,第一個打印的日志,在代碼交接給其他人或者在跟別人協(xié)作的時候,這個日志根本沒有價值,反而提升了大家看日志的難度。一個服務(wù)一跑起來,就瘋狂打日志,請求處理正常也打一堆日志。滾滾而來的日志,把錯誤日志淹沒在里面。錯誤日志失去了效果,簡單地tail查看日志,眼花繚亂,看不出任何問題,這不就成了'為了捕獲問題'而讓自己'根本無法捕獲問題'了么?沉默是金。除了簡單的statlog,如果你的程序'發(fā)聲'了,那么它拋出的信息就一定要有效!打印一個log('processfail')也是毫無價值,到底什么fail了?是哪個用戶帶著什么參數(shù)在哪個環(huán)節(jié)怎么fail了?如果發(fā)聲,就要把必要信息給全。不然就是不發(fā)聲,表示自己好好地work著呢。不發(fā)聲就是最好的消息,現(xiàn)在我的work一切正常!"設(shè)計良好的程序?qū)⒂脩舻淖⒁饬σ暈橛邢薜膶氋F資源,只有在必要時才要求使用。"程序員自己的主力,也是寶貴的資源!只有有必要的時候,日志才跑來提醒程序員'我有問題,來看看',而且,必須要給

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論