主頁 > 軟體工程 > 萬字詳文告訴你如何做 Code Review

萬字詳文告訴你如何做 Code Review

2022-12-25 07:13:48 軟體工程

 

 

前言

作為公司代碼委員會 golang 分會的理事,我 review 了很多代碼,看了很多別人的 review 評論,發現不少同學 code review 與寫出好代碼的水平有待提高,在這里,想分享一下我的一些理念和思路,

 

為什么技術人員包括 leader 都要做 code review

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

代碼,是設計理念落地的地方,是技術的呈現和根本,同學們可以在 review 程序中做到落地溝通,不再是空對空的討論,可以在實際問題中產生思考的碰撞,互相學習,大家都掌握團隊里積累出來最好的實踐方式!當然,如果 leader 沒時間寫代碼,僅僅是 review 代碼,指出其他同學某些實踐方式不好,要給出好的實踐的意見,即使沒親手寫代碼,也是對最佳實踐要有很多思考,

 

為什么同學們要在 review 中思考和總結最佳實踐

我這里先給一個我自己的總結:所謂架構師,就是掌握大量設計理念和原則、落地到各種語言及附帶工具鏈(生態)下的實踐方法、垂直行業模型理解,定制系統模型設計和工程實踐規范細則,進而控制 30+萬行代碼專案的開發便利性、可維護性、可測驗性、運營質量,

厲害的技術人,主要可以分為下面幾個方向:

  • 奇技淫巧

掌握很多技巧,以及發現技巧一系列思路,比如很多編程大賽,比的就是這個,但是,這個對工程,用處好像并不是很大,

  • 領域奠基

比如約翰*卡馬克,他創造出了現代計算機圖形高效渲染的方法論,不論如果沒有他,后面會不會有人發明,他就是第一個發明了,1999 年,卡馬克登上了美國時代雜志評選出來的科技領域 50 大影響力人物榜單,并且名列第 10 位,但是,類似的殿堂級位置,沒有幾個,不夠大家分,沒我們的事兒,

  • 理論研究

八十年代李開復博士堅持采用隱含馬爾可夫模型的框架,成功地開發了世界上第一個大詞匯量連續語音識別系統 Sphinx,我輩工程師,好像擅長這個的很少,

  • 產品成功

小龍哥是標桿,

  • 最佳實踐

這個是大家都可以做到,按照上面架構師的定義,在這條路上走得好,就能為任何公司組建技術團隊,組織建設高質量的系統,

從上面的討論中,可以看出,我們普通工程師的進化之路,就是不斷打磨最佳實踐方法論、落地細節,

 

代碼變壞的根源

在討論什么代碼是好代碼之前,我們先討論什么是不好的,計算機是人造的學科,我們自己制造了很多問題,進而去思考解法,

重復的代碼

// BatchGetQQTinyWithAdmin 獲取QQ uin的tinyID, 需要主uin的tiny和登錄態
// 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,
 }
 

因為最開始協議設計得不好,第一個使用介面的人,沒有類似上面這個函式的代碼,自己實作了一個嵌入邏輯代碼的填寫請求結構結構體的代碼,一開始,挺好的,但當有第二個人,第三個人干了類似的事情,我們將無法再重構這個協議,必須做到麻煩的向前兼容,而且每個同學,都要理解一遍上面這個協議怎么填,理解有問題,就觸發 bug,或者,如果某個錯誤的理解,普遍存在,我們就得找到所有這些重復的片段,都修改一遍,

當你要讀一個資料,發現兩個地方有,不知道該選擇哪個,當你要實作一個功能,發現兩個 rpc 介面、兩個函式能做到,你不知道選哪一個,你有面臨過這樣的'人生難題'么?其實怎么選并不重要了,你寫的這個代碼已經在走向 shit 的道路上邁出了堅實的一步,

但是,A little copying is better than a little dependency,這里提一嘴,不展開,

這里,我必須額外說一句,大家使用 trpc,感覺自己被鼓勵'每個服務搞一個 git',那,你這個服務里訪問 db 的代碼,rpc 的代碼,各種可以復用的代碼,是用的大家都復用的 git 下的代碼么?每次都重復寫一遍,db 欄位細節改了,每個使用過 db 的 server 對應的 git 都改一遍?這個通用 git 已經寫好的介面應該不知道哪些 git 下的代碼因為自己不向前兼容的修改而永遠放棄了向前不兼容的修改?

早期有效的決策不再有效

很多時候,我們第一版代碼寫出來,是沒有太大的問題的,比如,下面這個代碼

// 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 {
 // 獲取之前的資料
 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
}

現在看,這個代碼挺好的,長度沒超過 80 行,邏輯比價清晰,但是當 isMerge 這里判斷邏輯,如果加入更多的邏輯,把區域行數撐到 50 行以上,這個函式,味道就壞了,出現兩個問題:

1)函式內代碼不在一個邏輯層次上,閱讀代碼,本來在閱讀著頂層邏輯,突然就掉入了長達 50 行的 isMerge 的邏輯處理細節,還沒看完,讀者已經忘了前面的代碼講了什么,需要來回看,挑戰自己大腦的 cache 尺寸,

2)代碼有問題后,再新加代碼的同學,是改還是不改前人寫好的代碼呢?出 bug 誰來背?這是一個靈魂拷問,

過早的優化

這個大家聽了很多了,這里不贅述,

對合理性沒有苛求

'兩種寫法都 ok,你隨便挑一種吧','我這樣也沒什么吧',這是我經常聽到的話,

// 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()可以放在當前的位置,也可以放在 i.l.Lock()下面,做成 defer,兩種在最初構造的時候,好像都行,這個時候,很多同學態度就變得不堅決,實際上,這里必須是 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

這樣的修改,是極有可能發生的,它還是要變成 defer,那,為什么不一開始就是 defer,進入最合理的狀態?不一開始就進入最合理的狀態,在后續協作中,其他同學很可能犯錯!

總是面向物件/總喜歡封裝

我是軟體工程科班出身,學的第一門編程語言是 c++,教材是這本 ,當時自己讀完教材,初入程式設計之門,對于里面講的'封裝',驚為天人,多么美妙的設計啊,面向物件,多么智慧的設計啊,但是,這些年來,我看到了大牛'云風'對于'畢業生使用 mysql api 就喜歡搞個 class 封裝再用'的嘲諷;看到了各種莫名其妙的 class 定義;體會到了經常要去看一個莫名其妙的繼承樹,必須要把整個繼承樹整體讀明白才能確認一個細小的邏輯分支;多次體會到了我需要辛苦地壓抑住自己的抵觸情緒,去細度一個自作聰明的被封裝的代碼,確認我的 bug,除了 UI 類場景,我認為少用繼承、多用組合,

template<class _PKG_TYPE>
class CSuperAction : public CSuperActionBase {
  public:
    typedef _PKG_TYPE pkg_type;
    typedef CSuperAction<pkg_type> this_type;
    ...
}

這是 sspp 的代碼,CSuperAction 和 CSuperActionBase,一會兒 super,一會兒 base,Super 和 SuperBase 是在怎樣的兩個抽象層次上,不通讀代碼,沒人能讀明白,我想確認任何細節,都要把多個層次的代碼都通讀了,有什么封裝性可言?

好,你說是作者沒有把 class name 取得好,那,問題是,你能取得好么?一個剛入職的 T1.2 的同學能把 class name、class 樹設計得好么?即使是對簡單的業務模型,也需要無數次'壞'的物件抽象實踐,才能培養出一個具有合格的 class 抽象能力的同學,這對于大型卻松散的團隊協作,不是破壞性的?已經有了一套繼承樹,想要添加功能就只能在這個繼承樹里添加,以前的繼承樹不再適合新的需求,這個繼承樹上所有的 class,以及使用它們的地方,你都去改?不,是個正常人都會放棄,開始堆屎山,

封裝,就是我可以不關心實作,但是,做一個穩定的系統,每一層設計都可能出問題,abi,總有合適的用法和不合適的用法,真的存在我們能完全不關心封裝的部分是怎么實作的?不,你不能,bug 和性能問題,常常就出現在,你用了錯誤的用法去使用一個封裝好的函式,即使是 android、ios 的 api,golang、java 現成的 api,我們常常都要去探究實作,才能把 api 用好,那,我們是不是該一上來,就做一個透明性很強的函式,才更為合理?使用者想知道細節,進來吧,我的實作很易讀,你看看就明白,使用時不會迷路!對于邏輯復雜的函式,我們還要強調函式內部作業方式'可以讓讀者在大腦里想象呈現完整程序'的可現性,讓使用者輕松讀懂,有把握,使用時,不迷路!

根本沒有設計

這個最可怕,所有需求,上手就是一頓擼,'設計是什么東西?我一個檔案 5w 行,一個函式 5k 行,干不完需求?'從第一行代碼開始,就是無設計的,隨意地踩著滿地的泥坑,對于旁人的眼光沒有感覺,一個人獨舞,產出的代碼,完成了需求,毀滅了接手自己代碼的人,這個就不舉例了,每個同學應該都能在自己的專案類發現這種代碼,

 

必須形而上的思考

常常,同學們聽演講,公開課,就喜歡聽一些細枝末節的'干活',這沒有問題,但是,你干了幾年活,學習了多少干貨知識點?構建起自己的技術思考'面',進入立體的'工程思維',把技術細節和系統要滿足的需求在思考上連接起來了么?當聽一個需求的時候,你能思考到自己的 code package 該怎么組織,函式該怎么組織了么?

那,技術點要怎么和需求連接起來呢?答案很簡單,你需要在時間里總結,總結出一些明確的原則、思維程序,思考怎么去總結,特別像是在思考哲學問題,從一些瑣碎的細節中,由具體情況上升到一些原則、公理,同時,大家在接受原則時,不應該是接受和記住原則本身,而應該是結構原則,讓這個原則在自己這里重新推理一遍,自己完全掌握這個原則的適用范圍,

再進一步具體地說,對于工程最佳實踐的形而上的思考程序,就是:

把工程實踐中遇到的問題,從問題型別和解法型別,兩個角度去歸類,總結出一些有限適用的原則,就從點到了面,把諸多總結出的原則,組合應用到自己的專案代碼中,就是把多個面結合起來構建了一套立體的最佳實踐的方案,當你這套方案能適應 30w+行代碼的專案,超過 30 人的專案,你就架構師入門了!當你這個專案,是多端,多語言,代碼量超過 300w 行,參與人數超過 300 人,代碼質量依然很高,代碼依然在高效地自我迭代,每天消除掉過時的代碼,填充高質量的替換舊代碼和新生的代碼,恭喜你,你已經是一個很高級的架構師了!再進一步,你對某個業務模型有獨到或者全面的理解,構建了一套行業第一的解決方案,結合剛才高質量實作的能力,實作了這么一個專案,沒啥好說的,你已經是專家工程師了,級別再高,我就不了解了,不在這里討論,

那么,我們要重頭開始積累思考和總結?不,有一本書叫做《unix 編程藝術》,我在不同的時期分別讀了 3 遍,等一會,我講一些里面提到的,我覺得在騰訊尤其值得拿出來說的原則,這些原則,正好就能作為 code review 時大家判定代碼質量的準繩,但,在那之前,我得講一下另外一個很重要的話題,模型設計,

model 設計

沒讀過 oauth2.0 RFC,就去設計第三方授權登陸的人,終歸還要再發明一個撇腳的 oauth,

2012 年我剛畢業,我和一個去了廣州聯通公司的華南理工畢業生聊天,當時他說他作業很不開心,因為作業里不經常寫代碼,而且認為自己有 ACM 競賽金牌級的演算法熟練度+對 CPP 代碼的熟悉,寫下一個個指標操作記憶體,什么程式寫不出來,什么事情做不好,當時我覺得,挺有道理,編程工具在手,我什么事情做不了?

現在,我會告訴他,復雜如 linux 作業系統、Chromium 引擎、windows office,你做不了,原因是,他根本沒進入軟體工程的工程世界,不是會搬磚就能修出港珠澳大橋,但是,這么回答并不好,舉證用的論據離我們太遙遠了,見微知著,我現在會回答,你做不了,簡單如一個權限系統,你知道怎么做么?堆積一堆邏輯層次一維展開的 if else?簡單如一個共享檔案管理,你知道怎么做么?堆積一堆邏輯層次一維展開的 ife lse?你聯通有上萬臺服務器,你要怎么寫一個管理平臺?堆積一堆邏輯層次一維展開的 ife lse?

上來就是干,能實作上面提到的三個看似簡單的需求?想一想,亞馬遜、阿里云折騰了多少年,最后才找到了容器+Kubernetes 的大殺器,這里,需要谷歌多少年在 BORG 系統上的實踐,提出了優秀的服務編排領域模型,權限領域,有 RBAC、DAC、MAC 等等模型,到了業務,又會有細節的不同,如 Domain Driven Design 說的,沒有良好的領域思考和模型抽象,邏輯復雜度就是 n^2 指數級的,你得寫多少 ifelse,得思考多少可能的 if 路徑,來 cover 所有的不合符預期的情況,你必須要有 Domain 思考探索、model 拆解/抽象/構建的能力,有人問過我,要怎么有效地獲得這個能力?這個問題我沒能回答,就像是在問我,怎么才能獲得 MIT 博士的學術能力?我無法回答,唯一回答就是,進入某個領域,就是首先去看前人的思考,站在前人的肩膀上,再用上自己的通識能力,去進一步思考,至于怎么建立好的通識思考能力,可能得去常青藤讀個書吧:)或者,就在工程實踐中思考和鍛煉自己的這個能力!

同時,基于 model 設計的代碼,能更好地適應產品經理不斷變更的需求,比如說,一個 calendar(日歷)應用,簡單來想,不要太簡單!以'userid_date'為 key 記錄一個用戶的每日安排不就完成了么?只往前走一步,設計了一個任務,上限分發給 100w 個人,創建這么一個任務,是往 100w 個人下面添加一條記錄?你得改掉之前的設計,換 db,再往前走一步,要拉出某個用戶和某個人一起要參與的所有事務,是把兩個人的所有任務來做 join?好像還行,如果是和 100 個人一起參與的所有任務呢?100 個人的任務來 join?不現實了吧,好,你引入一個群組 id,那么,你最開始的'userid_date'為 key 的設計,是不是又要修改和做資料遷移了?經常來一個需求,你就得把系統推翻重來,或者根本就只能拒絕用戶的需求,這樣的戰斗力,還好意思叫自己工程師?你一開始就應該思考自己面對的業務領域,思考自己的日歷應用可能的模型邊界,把可能要做的能力都拿進來思考,構建一個 model,設計一套通用的 store 層介面,基于通用介面的邏輯代碼,當產品不斷發展,就是不停往模型里填內容,而不是推翻重來,這,思考模型邊界,構建模型細節,就是兩個很重要的能力,也是絕大多數騰訊產品經理不具備的能力,你得具備,對整個團隊都是極其有益的,你面對產品經理時,就聽取他們出于對用戶體驗負責思考出的需求點,到你自己這里,用一個完整的模型去涵蓋這些零碎的點,

model 設計,是形而上思考中的一個方面,一個特別重要的方面,接下來,我們來抄襲抄襲 unix 作業系統構建的實踐為我們提出的前人實踐經驗和'公理'總結,在自己的 coding/code review 中,站在巨人的肩膀上去思考,不重復地發現經典力學,而是往相對論挺進,

 

UNIX 設計哲學

不懂 Unix 的人注定最侄訓要重復發明一個撇腳的 Unix,--Henry Spenncer, 1987.11

下面這一段話太經典,我必須要摘抄一遍(自《UNIX 編程藝術》):“工程和設計的每個分支都有自己的技術文化,在大多數工程領域中,就一個專業人員的素養組成來說,有些不成文的行業素養具有與標準手冊及教科書同等重要的地位(并且隨著專業人員經驗的榷訓月累,這些經驗常常會比書本更重要),資深工程師們在作業中會積累大量的隱性知識,他們用類似禪宗'教外別傳'的方式,通過言傳身教傳授給后輩,軟體工程算是此規則的一個例外:技術變革如此之快,軟體環境日新月異,軟體技術文化暫如朝露,然而,例外之中也有例外,確有極少數軟體技術被證明經久耐用,足以演進為強勢的技術文化、有鮮明特色的藝術和世代相傳的設計哲學,“

接下來,我用我的理解,講解一下幾個我們常常做不到的原則,

Keep It Simple Stuped!

KISS 原則,大家應該是如雷貫耳了,但是,你真的在遵守?什么是 Simple?簡單?golang 語言主要設計者之一的 Rob Pike 說'大道至簡',這個'簡'和簡單是一個意思么?

首先,簡單不是面對一個問題,我們印入眼簾第一映像的解法為簡單,我說一句,感受一下,"把一個事情做出來容易,把事情用最簡單有效的方法做出來,是一個很難的事情,"比如,做一個三方授權,oauth2.0 很簡單,所有概念和細節都是緊湊、完備、易用的,你覺得要設計到 oauth2.0 這個效果很容易么?要做到簡單,就要對自己處理的問題有全面的了解,然后需要不斷積累思考,才能做到從各個角度和層級去認識這個問題,打磨出一個通俗、緊湊、完備的設計,就像 ios 的互動設計,簡單不是容易做到的,需要大家在不斷的時間和 code review 程序中去積累思考,pk 中觸發思考,交流中總結思考,才能做得愈發地好,接近'大道至簡',

兩張經典的模型圖,簡單又全面,感受一下,沒看懂,可以立即自行 google 學習一下:RBAC:圖片

logging:

圖片

原則 3 組合原則: 設計時考慮拼接組合

關于 OOP,關于繼承,我前面已經說過了,那我們怎么組織自己的模塊?對,用組合的方式來達到,linux 作業系統離我們這么近,它是怎么架構起來的?往小里說,我們一個串聯一個業務請求的資料集合,如果使用 BaseSession,XXXSession inherit BaseSession 的設計,其實,這個繼承樹,很難適應層出不窮的變化,但是如果使用組合,就可以拆解出 UserSignature 等等各種可能需要的部件,在需要的時候組合使用,不斷添加新的部件而沒有對老的繼承樹的記憶這個心智負擔,

使用組合,其實就是要讓你明確清楚自己現在所擁有的是哪個部件,如果部件過于多,其實完成組合最終成品這個步驟,就會有較高的心智負擔,每個部件展開來,琳瑯滿目,眼花繚亂,比如 QT 這個通用 UI 框架,看它的Class 串列,有 1000 多個,如果不用繼承樹把它組織起來,平鋪展開,組合出一個頁面,將會變得心智負擔高到無法承受,OOP 在'需要無數元素同時展現出來'這種復雜度極高的場景,有效的控制了復雜度 ,'那么,古爾丹,代價是什么呢?'代價就是,一開始做出這個自上而下的設計,牽一發而動全身,每次調整都變得例外困難,

實際專案中,各種職業級別不同的同學一起協作修改一個 server 的代碼,就會出現,職級低的同學改哪里都改不對,根本沒能力進行修改,高級別的同學能修改對,也不愿意大規模修改,整個專案變得愈發不合理,對整個繼承樹沒有完全認識的同學都沒有資格進行任何一個對繼承樹有調整的修改,協作變得寸步難行,代碼的修改,都變成了依賴一個高級架構師高強度監控繼承體系的變化,低級別同學們束手束腳的結果,組合,就很好的解決了這個問題,把問題不斷細分,每個同學都可以很好地攻克自己需要攻克的點,實作一個 package,產品邏輯代碼,只需要去組合各個 package,就能達到效果,

這是 golang 標準庫里 http request 的定義,它就是 Http 請求所有特性集合出來的結果,其中通用/異變/多種實作的部分,通過 duck interface 抽象,比如 Body io.ReadCloser,你想知道哪些細節,就從組合成 request 的部件入手,要修改,只需要修改對應部件,[這段代碼后,對比.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 里對于 web 服務的抽象,僅僅看到末端,不去看完整個繼承樹的完整圖景,我根本無法知道我關心的某個細節在什么位置,進而,我要往整個 http 服務體系里修改任何功能,都無法拋開對整體完整設計的理解和熟悉,還極容易沒有知覺地破壞者整體的設計,

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

原則 6 吝嗇原則: 除非確無它法, 不要撰寫龐大的程式

可能有些同學會覺得,把程式寫得龐大一些才好拿得出手去評 T11、T12,leader 們一看評審方案就容易覺得:很大,很好,很全面,但是,我們真的需要寫這么大的程式么?

我又要說了"那么,古爾丹,代價是什么呢?",代價是代碼越多,越難維護,難調整,C 語言之父 Ken Thompson 說"洗掉一行代碼,給我帶來的成就感要比添加一行要大",我們對于代碼,要吝嗇,能把系統做小,就不要做大,騰訊不乏 200w+行的客戶端,很大,很牛,但是,同學們自問,現在還調整得動架構么,手 Q 的同學們,看看自己代碼,曾經嘆息過么,能小做的事情就小做,尋求通用化,通過 duck interface(甚至多行程,用于隔離能力的多執行緒)把模塊、能力隔離開,時刻想著刪減代碼量,才能保持代碼的可維護性和面對未來的需求、架構,調整自身的活力,客戶端代碼,UI 渲染模塊可以復雜吊炸天,非 UI 部分應該追求最簡單,能力介面化,可替換、重組合能力強,

落地到大家的代碼,review 時,就應該最關注核心 struct 定義,構建起一個完備的模型,核心 interface,明確抽象 model 對外部的依賴,明確抽象 model 對外提供的能力,其他代碼,就是要用最簡單、平平無奇的代碼實作模型內部細節,

原則 7 透明性原則: 設計要可見,以便審查和除錯

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

"如果沒有陰暗的角落和隱藏的深度,軟體系統就是透明的,透明性是一種被動的品質,如果實際上能預測到程式行為的全部或大部分情況,并能建立簡單的心理模型,這個程式就是透明的,因為可以看透機器究竟在干什么,

如果軟體系統所包含的功能是為了幫助人們對軟體建立正確的'做什么、怎么做'的心理模型而設計,這個軟體系統就是可顯的,因此,舉例來說,對用戶而言,良好的檔案有助于提高可顯性;對程式員而言,良好的變數和函式名有助于提高可顯性,可顯性是一種主動品質,在軟體中要達到這一點,僅僅做到不晦澀是不夠的,還必須要盡力做到有幫助,"

我們要寫好程式,減少 bug,就要增強自己對代碼的控制力,你始終做到,理解自己呼叫的函式/復用的代碼大概是怎么實作的,不然,你可能就會在單執行緒狀態機的 server 里呼叫有 IO 阻塞的函式,讓自己的 server 吞吐量直接掉到底,進而,為了保證大家能對自己代碼能做到有控制力,所有人寫的函式,就必須具備很高的透明性,而不是寫一些看了一陣看不明白的函式/代碼,結果被迫使用你代碼的人,直接放棄了對掌控力的追取,甚至放棄復用你的代碼,另起爐灶,走向了'制造重復代碼'的深淵,

透明性其實相對容易做到的,大家有意識地鍛煉一兩個月,就能做得很好,可顯性就不容易了,有一個現象是,你寫的每一個函式都不超過 80 行,每一行我都能看懂,但是你層層呼叫,很多函式呼叫,組合起來怎么就實作了某個功能,看兩遍,還是看不懂,第三遍可能才能大概看懂,大概看懂了,但太復雜,很難在大腦里構建起你實作這個功能的整體流程,結果就是,閱讀者根本做不到對你的代碼有好的掌控力,

可顯性的標準很簡單,大家看一段代碼,懂不懂,一下就明白了,但是,如何做好可顯性?那就是要追求合理的函式分組,合理的函式上下級層次,同一層次的代碼才會出現在同一個函式里,追求通俗易懂的函式分組分層方式,是通往可顯性的道路,

當然,復雜如 linux 作業系統,office 檔案,問題本身就很復雜,拆解、分層、組合得再合理,都難建立心理模型,這個時候,就需要完備的檔案了,完備的檔案還需要出現在離代碼最近的地方,讓人'知道這里復雜的邏輯有檔案',而不是其實檔案,但是閱讀者不知道,再看看上面 golang 標準庫里的 http.Request,感受到它在可顯性上的努力了么?對,就去學它,

原則 10 通俗原則: 介面設計避免標新立異

設計程式過于標新立異的話,可能會提升別人理解的難度,

一般,我們這么定義一個'點',使用 x 表示橫坐標,用 y 表示縱坐標:

type Point struct {
 X float64
 Y float64
}

你就是要不同、精準:

type Point struct {
 VerticalOrdinate   float64
 HorizontalOrdinate float64
}

很好,你用詞很精準,一般人還駁斥不了你,但是,多數人讀你的 VerticalOrdinate 就是沒有讀 X 理解來得快,來得容易懂、方便,你是在刻意制造協作成本,

上面的例子常見,但還不是最小立異原則最想說明的問題,想想一下,一個程式里,你把用'+'這個符號表示陣列添加元素,而不是數學'加','result := 1+2' --> 'result = []int{1, 2}'而不是'result=3',那么,你這個標新立異,對程式的破壞性,簡直無法想象,"最小立異原則的另一面是避免表象想死而實際卻略有不同,這會極端危險,因為表象相似往往導致人們產生錯誤的假定,所以最好讓不同事物有明顯區別,而不要看起來幾乎一模一樣," -- Henry Spencer,

你實作一個 db.Add()函式卻做著 db.AddOrUpdate()的操作,有人使用了你的介面,錯誤地把資料覆寫了,

原則 11 緘默原則: 如果一個程式沒什么好說的,就沉默

這個原則,應該是大家最經常破壞的原則之一,一段簡短的代碼里插入了各種'log("cmd xxx enter")', 'log("req data " + req.String())',非常害怕自己資訊列印得不夠,害怕自己不知道程式執行成功了,總要最后'log("success")',但是,我問一下大家,你們真的耐心看過別人寫的代碼打的一堆日志么?不是自己需要哪個,就在一堆日志里,再列印一個日志出來一個帶有特殊標記的日志'log("this_is_my_log_" + xxxxx)'?結果,第一個作者列印的日志,在代碼交接給其他人或者在跟別人協作的時候,這個日志根本沒有價值,反而提升了大家看日志的難度,

一個服務一跑起來,就瘋狂打日志,請求處理正常也打一堆日志,滾滾而來的日志,把錯誤日志淹沒在里面,錯誤日志失去了效果,簡單地 tail 查看日志,眼花繚亂,看不出任何問題,這不就成了'為了捕獲問題'而讓自己'根本無法捕獲問題'了么?

沉默是金,除了簡單的 stat log,如果你的程式'發聲'了,那么它拋出的資訊就一定要有效!列印一個 log('process fail')也是毫無價值,到底什么 fail 了?是哪個用戶帶著什么引數在哪個環節怎么 fail 了?如果發聲,就要把必要資訊給全,不然就是不發聲,表示自己好好地 work 著呢,不發聲就是最好的訊息,現在我的 work 一切正常!

"設計良好的程式將用戶的注意力視為有限的寶貴資源,只有在必要時才要求使用,"程式員自己的主力,也是寶貴的資源!只有有必要的時候,日志才跑來提醒程式員'我有問題,來看看',而且,必須要給到足夠的資訊,讓一把講明白現在發生了什么,而不是程式員還需要很多輔助手段來搞明白到底發生了什么,

每當我發布程式 ,我抽查一個機器,看它的日志,發現只有每分鐘外部接入、內部 rpc 的個數/延時分布日志的時候,我就心情很愉悅,我知道,這一分鐘,它的成功率又是 100%,沒任何問題!

原則 12 補救原則: 出現例外時,馬上退出并給出足夠錯誤資訊

其實這個問題很簡單,如果出現例外,例外并不會因為我們嘗試掩蓋它,它就不存在了,所以,程式錯誤和邏輯錯誤要嚴格區分對待,這是一個態度問題,

'例外是互聯網服務器的常態',邏輯錯誤通過 metrics 統計,我們做好告警分析,對于程式錯誤 ,我們就必須要嚴格做到在問題最早出現的位置就把必要的資訊搜集起來,高調地告知開發和維護者'我出現例外了,請立即修復我!',可以是直接就沒有被捕獲的 panic 了,也可以在一個最上層的位置統一做好 recover 機制,但是在 recover 的時候一定要能獲得準確例外位置的準確例外資訊,不能有中間 catch 機制,catch 之后丟失很多資訊再往上傳遞,

很多 Java 開發的同學,不區分程式錯誤和邏輯錯誤,要么都很寬容,要么都很嚴格,對代碼的可維護性是毀滅性的破壞,"我的程式沒有程式錯誤,如果有,我當時就解決了,"只有這樣,才能保持程式代碼質量的相對穩定,在火苗出現時撲滅火災是最好的撲滅火災的方式,當然,更有效的方式是全面自動化測驗的預防:)

 

具體實踐點

前面提了好多思考方向的問題,大的原則問題和方向,我這里,再來給大家簡單列舉幾個細節執行點吧,畢竟,大家要上手,是從執行開始,然后才是總結思考,能把我的思考方式抄過去,下面是針對 golang 語言的,其他語言略有不同,以及,我一時也想不全我所執行的 所有細則,這就是我強調'原則'的重要性,原則是可列舉的,

  • 對于代碼格式規范,100%嚴格執行,嚴重容不得一點沙,

  • 檔案絕不能超過 800 行,超過,一定要思考怎么拆檔案,工程思維,就在于拆檔案的時候積累,

  • 函式對決不能超過 80 行,超過,一定要思考怎么拆函式,思考函式分組,層次,工程思維,就在于拆檔案的時候積累,

  • 代碼嵌套層次不能超過 4 層,超過了就得改,多想想能不能 early return,工程思維,就在于拆檔案的時候積累,

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

doB()
return

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

  • 從目錄、package、檔案、struct、function 一層層下來 ,資訊一定不能出現冗余,比如 file.FileProperty 這種定義,只有每個'定語'只出現在一個位置,才為'做好邏輯、定義分組/分層'提供了可能性,
  • 多用多級目錄來組織代碼所承載的資訊,即使某一些中間目錄只有一個子目錄,
  • 隨著代碼的擴展,老的代碼違反了一些設計原則,應該立即原地區域重構,維持住代碼質量不滑坡,比如:拆檔案;拆函式;用 Session 來保存一個復雜的流程型函式的所有資訊;重新調整目錄結構,
  • 基于上一點考慮,我們應該盡量讓專案的代碼有一定的組織、層次關系,我個人的當前實踐是除了特別通用的代碼,都放在一個 git 里,特別通用、修改少的代碼,逐漸獨立出 git,作為子 git 連接到當前專案 git,讓 goland 的 Refactor 特性、各種 Refactor 工具能幫助我們快速、安全域部重構,
  • 自己的專案代碼,應該有一個內生的層級和邏輯關系,flat 平鋪展開是非常不利于代碼復用的,怎么復用、怎么組織復用,肯定會變成'人生難題',T4-T7 的同學根本無力解決這種難題,
  • 如果被 review 的代碼雖然簡短,但是你看了一眼卻發現不咋懂,那就一定有問題,自己看不出來,就找高級別的同學交流,這是你和別 review 代碼的同學成長的時刻,
  • 日志要少打,要打日志就要把關鍵索引資訊帶上,必要的日志必須打,
  • 有疑問就立即問,不要怕問錯,讓代碼作者給出解釋,不要怕問出極低問題,
  • 不要說'建議',提問題,就是剛,你 pk 不過我,就得改!
  • 請積極使用 trpc,總是要和老板站在一起!只有和老板達成的對于代碼質量建設的共識,才能在團隊里更好地做好代碼質量建設,
  • 消滅重復!消滅重復!消滅重復!

 

主干開發

最后,我來為'主干開發'多說一句話,道理很簡單,只有每次被 review 代碼不到 500 行,reviewer 才能快速地看完,而且幾乎不會看漏,超過 500 行,reviewer 就不能仔細看,只能大概瀏覽了,而且,讓你調整 500 行代碼內的邏輯比調整 3000 行甚至更多的代碼,容易很多,降低不僅僅是 6 倍,而是一到兩個數量級,有問題,在剛出現的時候就調整了,不會給被 revew 的人帶來大的修改負擔,

關于 CI(continuous integration),還有很多好的資料和書籍,大家應該及時去學習學習,

《unix 編程藝術》

建議大家把這本書找出來讀一讀,特別是,T7 及更高級別的同學,你們已經積累了大量的代碼實踐,亟需對'工程性'做思考總結,很多工程方法論都過時了,這本書的內容,是例外中的例外,它所表達出的內容沒有因為軟體技術的不斷更替而過時,

佛教禪宗講'不立文字'(不立文字,教外別傳,直指人心,見性成佛),很多道理和感悟是不能用文字傳達的,文字的表達能力,不能表達,大家常常因為"自己聽說過、知道某個道理"而產生一種安心感,認為"我懂了這個道理",但是自己卻不能在實踐中做到,知易行難,知道卻做不到,在工程實踐里,就和'不懂這個道理'沒有任何區別了,

曾經,我面試過一個別的公司的總監,講得好像一套一套,代碼拉出來遛一遛,根本就沒做到,僅僅會道聽途說,他在工程實踐上的探索前路可以說已經基本斷絕了,我只能祝君能做好向上管理,走自己的純管理道路吧,請不要再說自己對技術有追求,是個技術人了!

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

Software Engineering at Google也是一本必讀好書,可惜沒找到中文翻譯,

 

作者:cheaterlin

本文來自博客園,作者:古道輕風,轉載請注明原文鏈接:https://www.cnblogs.com/88223100/p/In-detail-tell-you-how-to-do-Code-Review.html

轉載請註明出處,本文鏈接:https://www.uj5u.com/gongcheng/540687.html

標籤:其他

上一篇:萬字詳文告訴你如何做 Code Review

下一篇:如何存盤 Git 大檔案?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • Git本地庫既關聯GitHub又關聯Gitee

    創建代碼倉庫 使用gitee舉例(github和gitee差不多) 1.在gitee右上角點擊+,選擇新建倉庫 ? 2.選擇填寫倉庫資訊,然后進行創建 ? 3.服務端已經準備好了,本地開始作準備 (1)Git 全域設定 git config --global user.name "成鈺" git c ......

    uj5u.com 2020-09-10 05:04:14 more
  • CODING DevOps 代碼質量實戰系列第二課,相約周三

    隨著 ToB(企業服務)的興起和 ToC(消費互聯網)產品進入成熟期,線上故障帶來的損失越來越大,代碼質量越來越重要,而「質量內建」正是 DevOps 核心理念之一。**《DevOps 代碼質量實戰(PHP 版)》**為 CODING DevOps 代碼質量實戰系列的第二課,同時也是本系列的 PHP ......

    uj5u.com 2020-09-10 05:07:43 more
  • 推薦Scrum書籍

    推薦Scrum書籍 直接上干貨,推薦書籍清單如下(推薦有順序的哦) Scrum指南 Scrum精髓 Scrum敏捷軟體開發 Scrum捷徑 硝煙中的Scrum和XP : 我們如何實施Scrum 敏捷軟體開發:Scrum實戰指南 Scrum要素 大規模Scrum:大規模敏捷組織的設計 用戶故事地圖 用 ......

    uj5u.com 2020-09-10 05:07:45 more
  • CODING DevOps 代碼質量實戰系列最后一課,周四發車

    隨著 ToB(企業服務)的興起和 ToC(消費互聯網)產品進入成熟期,線上故障帶來的損失越來越大,代碼質量越來越重要,而「質量內建」正是 DevOps 核心理念之一。 **《DevOps 代碼質量實戰(Java 版)》**為 CODING DevOps 代碼質量實戰系列的最后一課,同時也是本系列的 ......

    uj5u.com 2020-09-10 05:07:52 more
  • 敏捷軟體工程實踐書籍

    Scrum轉型想要做好,第一步先了解并真正落實Scrum,那么我推薦的Scrum書籍是要看懂并實踐的。第二步是團隊的工程實踐要做扎實。 下面推薦工程實踐書單: 重構:改善既有代碼的設計 決議極限編程 : 擁抱變化 代碼整潔代碼 程式員的職業素養 修改代碼的藝術 撰寫可讀代碼的藝術 測驗驅動開發 : ......

    uj5u.com 2020-09-10 05:07:55 more
  • Jenkins+svn+nginx實作windows環境自動部署vue前端專案

    前面文章介紹了Jenkins+svn+tomcat實作自動化部署,現在終于有空抽時間出來寫下Jenkins+svn+nginx實作自動部署vue前端專案。 jenkins的安裝和配置已經在前面文章進行介紹,下面介紹實作vue前端專案需要進行的哪些額外的步驟。 注意:在安裝jenkins和nginx的 ......

    uj5u.com 2020-09-10 05:08:49 more
  • CODING DevOps 微服務專案實戰系列第一課,明天等你

    CODING DevOps 微服務專案實戰系列第一課**《DevOps 微服務專案實戰:DevOps 初體驗》**將由 CODING DevOps 開發工程師 王寬老師 向大家介紹 DevOps 的基本理念,并探討為什么現代開發活動需要 DevOps,同時將以 eShopOnContainers 項 ......

    uj5u.com 2020-09-10 05:09:14 more
  • CODING DevOps 微服務專案實戰系列第二課來啦!

    近年來,工程專案的結構越來越復雜,需要接入合適的持續集成流水線形式,才能滿足更多變的需求,那么如何優雅地使用 CI 能力提升生產效率呢?CODING DevOps 微服務專案實戰系列第二課 《DevOps 微服務專案實戰:CI 進階用法》 將由 CODING DevOps 全堆疊工程師 何晨哲老師 向 ......

    uj5u.com 2020-09-10 05:09:33 more
  • CODING DevOps 微服務專案實戰系列最后一課,周四開講!

    隨著軟體工程越來越復雜化,如何在 Kubernetes 集群進行灰度發布成為了生產部署的”必修課“,而如何實作安全可控、自動化的灰度發布也成為了持續部署重點關注的問題。CODING DevOps 微服務專案實戰系列最后一課:**《DevOps 微服務專案實戰:基于 Nginx-ingress 的自動 ......

    uj5u.com 2020-09-10 05:10:00 more
  • CODING 儀表盤功能正式推出,實作作業資料可視化!

    CODING 儀表盤功能現已正式推出!該功能旨在用一張張統計卡片的形式,統計并展示使用 CODING 中所產生的資料。這意味著無需額外的設定,就可以收集歸納寶貴的作業資料并予之量化分析。這些海量的資料皆會以圖表或串列的方式躍然紙上,方便團隊成員隨時查看各專案的進度、狀態和指標,云端協作迎來真正意義上 ......

    uj5u.com 2020-09-10 05:11:01 more
最新发布
  • windows系統git使用ssh方式和gitee/github進行同步

    使用git來clone專案有兩種方式:HTTPS和SSH:
    HTTPS:不管是誰,拿到url隨便clone,但是在push的時候需要驗證用戶名和密碼;
    SSH:clone的專案你必須是擁有者或者管理員,而且需要在clone前添加SSH Key。SSH 在push的時候,是不需要輸入用戶名的,如果配置... ......

    uj5u.com 2023-04-19 08:41:12 more
  • windows系統git使用ssh方式和gitee/github進行同步

    使用git來clone專案有兩種方式:HTTPS和SSH:
    HTTPS:不管是誰,拿到url隨便clone,但是在push的時候需要驗證用戶名和密碼;
    SSH:clone的專案你必須是擁有者或者管理員,而且需要在clone前添加SSH Key。SSH 在push的時候,是不需要輸入用戶名的,如果配置... ......

    uj5u.com 2023-04-19 08:35:34 more
  • 2023年農牧行業6大CRM系統、5大場景盤點

    在物聯網、大資料、云計算、人工智能、自動化技術等現代資訊技術蓬勃發展與逐步成熟的背景下,數字化正成為農牧行業供給側結構性變革與高質量發展的核心驅動因素。因此,改造和提升傳統農牧業、開拓創新現代智慧農牧業,加快推進農牧業的現代化、資訊化、數字化建設已成為農牧業發展的重要方向。 當下,企業數字化轉型已經 ......

    uj5u.com 2023-04-18 08:05:44 more
  • 2023年農牧行業6大CRM系統、5大場景盤點

    在物聯網、大資料、云計算、人工智能、自動化技術等現代資訊技術蓬勃發展與逐步成熟的背景下,數字化正成為農牧行業供給側結構性變革與高質量發展的核心驅動因素。因此,改造和提升傳統農牧業、開拓創新現代智慧農牧業,加快推進農牧業的現代化、資訊化、數字化建設已成為農牧業發展的重要方向。 當下,企業數字化轉型已經 ......

    uj5u.com 2023-04-18 08:00:18 more
  • 計算機組成原理—存盤器

    計算機組成原理—硬體結構 二、存盤器 1.概述 存盤器是計算機系統中的記憶設備,用來存放程式和資料 1.1存盤器的層次結構 快取-主存層次主要解決CPU和主存速度不匹配的問題,速度接近快取 主存-輔存層次主要解決存盤系統的容量問題,容量接近與價位接近于主存 2.主存盤器 2.1概述 主存與CPU的聯 ......

    uj5u.com 2023-04-17 08:20:31 more
  • 談一談我對協同開發的一些認識

    如今各互聯網公司普通都使用敏捷開發,采用小步快跑的形式來進行專案開發。如果是小專案或者小需求,那一個開發可能就搞定了。但對于電商等復雜的系統,其功能多,結構復雜,一個人肯定是搞不定的,所以都是很多人來共同開發維護。以我曾經待過的商城團隊為例,光是后端開發就有七十多人。 為了更好地開發這類大型系統,往 ......

    uj5u.com 2023-04-17 08:18:55 more
  • 專案管理PRINCE2核心知識點整理

    PRINCE2,即 PRoject IN Controlled Environment(受控環境中的專案)是一種結構化的專案管理方法論,由英國政府內閣商務部(OGC)推出,是英國專案管理標準。
    PRINCE2 作為一種開放的方法論,是一套結構化的專案管理流程,描述了如何以一種邏輯性的、有組織的方法,... ......

    uj5u.com 2023-04-17 08:18:51 more
  • 談一談我對協同開發的一些認識

    如今各互聯網公司普通都使用敏捷開發,采用小步快跑的形式來進行專案開發。如果是小專案或者小需求,那一個開發可能就搞定了。但對于電商等復雜的系統,其功能多,結構復雜,一個人肯定是搞不定的,所以都是很多人來共同開發維護。以我曾經待過的商城團隊為例,光是后端開發就有七十多人。 為了更好地開發這類大型系統,往 ......

    uj5u.com 2023-04-17 08:18:00 more
  • 專案管理PRINCE2核心知識點整理

    PRINCE2,即 PRoject IN Controlled Environment(受控環境中的專案)是一種結構化的專案管理方法論,由英國政府內閣商務部(OGC)推出,是英國專案管理標準。
    PRINCE2 作為一種開放的方法論,是一套結構化的專案管理流程,描述了如何以一種邏輯性的、有組織的方法,... ......

    uj5u.com 2023-04-17 08:17:55 more
  • 計算機組成原理—存盤器

    計算機組成原理—硬體結構 二、存盤器 1.概述 存盤器是計算機系統中的記憶設備,用來存放程式和資料 1.1存盤器的層次結構 快取-主存層次主要解決CPU和主存速度不匹配的問題,速度接近快取 主存-輔存層次主要解決存盤系統的容量問題,容量接近與價位接近于主存 2.主存盤器 2.1概述 主存與CPU的聯 ......

    uj5u.com 2023-04-17 08:12:06 more