<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          滴滴:如何提高代碼的可讀性,以 Go 為例!

          共 23873字,需瀏覽 48分鐘

           ·

          2021-02-28 20:37

          本文整理自 taowen 師傅在滴滴內(nèi)部的分享。

          1.Why

          對(duì)一線開發(fā)人員來說,每天工作內(nèi)容大多是在已有項(xiàng)目的基礎(chǔ)上繼續(xù)堆代碼。當(dāng)項(xiàng)目實(shí)在堆不動(dòng)時(shí)就需要尋找收益來重構(gòu)代碼。既然我們的大多數(shù)時(shí)間都花在坐在顯示器前讀寫代碼這件事上,那可讀性不好的代碼都是在謀殺自己or同事的生命,所以不如一開始就提煉技巧,努力寫好代碼; )

          2.How

          為提高代碼可讀性,先來分析代碼實(shí)際運(yùn)行環(huán)境。代碼實(shí)際運(yùn)行于兩個(gè)地方:cpu人腦。對(duì)于cpu,代碼優(yōu)化需理解其工作機(jī)制,寫代碼時(shí)為針對(duì)cpu特性進(jìn)行優(yōu)化;對(duì)于人腦,我們?cè)谧x代碼時(shí),它像解釋器一樣,一行一行運(yùn)行代碼,從這個(gè)角度來說,要提高代碼的可讀性首先需要知道大腦的運(yùn)行機(jī)制。

          下面來看一下人腦適合做的事情和不適合做的事情:

          大腦擅長(zhǎng)做的事情

          名稱圖片說明
          對(duì)象識(shí)別

          不同于機(jī)器學(xué)習(xí)看無(wú)數(shù)張貓片之后可能還是不能準(zhǔn)確識(shí)別貓這個(gè)對(duì)象,人腦在看過幾只貓之后就可以很好的識(shí)別。
          空間分解

          人腦不需要標(biāo)注,可以直觀感受到空間中的不同物體。
          時(shí)序預(yù)測(cè)

          你的第一感覺是不是這個(gè)哥們要被車撞了?
          時(shí)序記憶

          作為人類生存本能之一,我們多次走過某個(gè)地方時(shí),人腦會(huì)對(duì)這個(gè)地方形成記憶。
          類比推測(cè)

          人腦還有類比功能,比如說這道題大多數(shù)人會(huì)選擇C吧。

          大腦不擅長(zhǎng)做的事情

          名稱圖片例子
          無(wú)法映射到現(xiàn)實(shí)生活經(jīng)驗(yàn)的抽象概念

          人腦看到左圖時(shí),會(huì)比較輕松想到通關(guān)方式,但是如果換成右圖這種抽象的概念,里面的對(duì)象換成了嘿嘿的像素,我們就不知道這是什么鬼了。比如說代碼里如果充斥著Z,X,C,V 這樣的變量名,你可能就看懵了。
          冗長(zhǎng)的偵探推理

          這種需要遞歸(or循環(huán))去檢查所有可能性最后找到解法的場(chǎng)景,人腦同樣不擅長(zhǎng)。
          跟蹤多個(gè)同時(shí)變化的過程

          大腦是個(gè)單線程的CPU,不擅長(zhǎng)左手畫圓,右手畫圈。

          代碼優(yōu)化理論

          了解人腦的優(yōu)缺點(diǎn)后,寫代碼時(shí)就可以根據(jù)人腦的特點(diǎn)對(duì)應(yīng)改善代碼的可讀性了。這里提取出三種理論:

          1. Align Models ,匹配模型:代碼中的數(shù)據(jù)和算法模型 應(yīng)和人腦中的 心智模型對(duì)應(yīng)

          2. Shorten Process , 簡(jiǎn)短處理:寫代碼時(shí)應(yīng) 縮短 “福爾摩斯探案集” 的流程長(zhǎng)度,即不要寫大段代碼

          3. Isolate Process,隔離處理:寫代碼一個(gè)流程一個(gè)流程來處理,不要同時(shí)描述多個(gè)流程的演進(jìn)過程

          下面通過例子詳細(xì)解釋這三種模型:

          Align Models

          在代碼中,模型無(wú)外乎就是數(shù)據(jù)結(jié)構(gòu)算法,而在人腦中,對(duì)應(yīng)的是心智模型,所謂心智模型就是人腦對(duì)于一個(gè)物體 or 一件事情的想法,我們平時(shí)說話就是心智模型的外在表現(xiàn)。寫代碼時(shí)應(yīng)把代碼中的名詞與現(xiàn)實(shí)名詞對(duì)應(yīng)起來,減少人腦從需求文檔到代碼的映射成本。比如對(duì)于“銀行賬戶”這個(gè)名詞,很多變量名都可以體現(xiàn)這個(gè)詞,比如:bankAccount、bank_account、account、BankAccount、BA、bank_acc、item、row、record、model,編碼中應(yīng)統(tǒng)一使用和現(xiàn)實(shí)對(duì)象能鏈接上的變量名。

          代碼命名技巧

          起變量名時(shí)候取其實(shí)際含義,沒必要隨便寫個(gè)變量名然后在注釋里面偷偷用功。

          // bad
          var d int // elapsed time in days

          // good
          var elapsedTimeInDays int // 全局使用

          起函數(shù)名時(shí) 動(dòng)詞+名詞結(jié)合,還要注意標(biāo)識(shí)出你的自定義變量類型:

          // bad
          func getThem(theList [][]int) [][]int {
           var list1 [][]int // list1是啥,不知道
           for _, x := range theList {
            if x[0] == 4 { // 4是啥,不知道
             list1 = append(list1, x)
            }
           }
           return list1
          }

          // good
          type Cell []int // 標(biāo)識(shí)[]int作用

          func (cell Cell) isFlagged() bool { // 說明4的作用
           return cell[0] == 4
          }

          func getFlaggedCells(gameBoard []Cell) []Cell { // 起有意義的變量名
           var flaggedCells []Cell
           for _, cell := range gameBoard {
            if cell.isFlagged() {
             flaggedCells = append(flaggedCells, cell)
            }
           }
           return flaggedCells
          }
          代碼分解技巧

          按照空間分解(Spatial Decomposition):下面這塊代碼都是與Page相關(guān)的邏輯,仔細(xì)觀察可以根據(jù)page的空間分解代碼:

          // bad
          // …then…and then … and then ... // 平鋪直敘描述整個(gè)過程
          func RenderPage(request *http.Request) map[string]interface{} {
           page := map[string]interface{}{}
           name := request.Form.Get("name")
           page["name"] = name
           urlPathName := strings.ToLower(name)
           urlPathName = regexp.MustCompile(`['.]`).ReplaceAllString(
            urlPathName, "")
           urlPathName = regexp.MustCompile(`[^a-z0-9]+`).ReplaceAllString(
            urlPathName, "-")
           urlPathName = strings.Trim(urlPathName, "-")
           page["url"] = "/biz/" + urlPathName
           page["date_created"] = time.Now().In(time.UTC)
           return page
          }
          // good
          // 按空間分解,這樣的好處是可以集中精力到關(guān)注的功能上
          var page = map[string]pageItem{
           "name":         pageName,
           "url":          pageUrl,
           "date_created": pageDateCreated,
          }

          type pageItem func(*http.Request) interface{}

          func pageName(request *http.Request) interface{} { // name 相關(guān)過程
           return request.Form.Get("name")
          }

          func pageUrl(request *http.Request) interface{} { // URL 相關(guān)過程
           name := request.Form.Get("name")
           urlPathName := strings.ToLower(name)
           urlPathName = regexp.MustCompile(`['.]`).ReplaceAllString(
            urlPathName, "")
           urlPathName = regexp.MustCompile(`[^a-z0-9]+`).ReplaceAllString(
            urlPathName, "-")
           urlPathName = strings.Trim(urlPathName, "-")
           return "/biz/" + urlPathName
          }

          func pageDateCreated(request *http.Request) interface{} { // Date 相關(guān)過程
           return time.Now().In(time.UTC)
          }

          按照時(shí)間分解(Temporal Decomposition):下面這塊代碼把整個(gè)流程的算賬和打印賬單混寫在一起,可以按照時(shí)間順序?qū)R進(jìn)行分解:

          // bad 
          func (customer *Customer) statement() string {
           totalAmount := float64(0)
           frequentRenterPoints := 0
           result := "Rental Record for " + customer.Name + "\n"

           for _, rental := range customer.rentals {
            thisAmount := float64(0)
            switch rental.PriceCode {
            case REGULAR:
             thisAmount += 2
            case New_RELEASE:
             thisAmount += rental.rent * 2
            case CHILDREN:
             thisAmount += 1.5
            }
            frequentRenterPoints += 1
            totalAmount += thisAmount
           }
           result += strconv.FormatFloat(totalAmount,'g',10,64) + "\n"
           result += strconv.Itoa(frequentRenterPoints)

           return result
          }
          // good 邏輯分解后的代碼
          func statement(custom *Customer) string {
           bill := calcBill(custom)

           statement := bill.print()

           return statement
          }

          type RentalBill struct {
           rental Rental
           amount float64
          }

          type Bill struct {
           customer             *Customer
           rentals              []RentalBill
           totalAmount          float64
           frequentRenterPoints int
          }

          func calcBill(customer *Customer) Bill {

           bill := Bill{}
           for _, rental := range customer.rentals {
            rentalBill := RentalBill{
             rental: rental,
             amount: calcAmount(rental),
            }
            bill.frequentRenterPoints += calcFrequentRenterPoints(rental)
            bill.totalAmount += rentalBill.amount
            bill.rentals = append(bill.rentals, rentalBill)
           }
           return bill
          }

          func (bill Bill) print() string {

           result := "Rental Record for " + bill.customer.name + "(n"

           for _, rental := range bill.rentals{
            result += "\t" + rental.movie.title + "\t" +
             strconv.FormatFloat(rental.amount, 'g'1064) + "\n"
           }
           

           result += "Amount owed is " +
            strconv.FormatFloat(bill.totalAmount, 'g'1064) + "\n"

           result += "You earned + " +
            strconv.Itoa(bill.frequentRenterPoints) + "frequent renter points"

           return result
          }

          func calcAmount(rental Rental) float64 {
           thisAmount := float64(0)
           switch rental.movie.priceCode {
           case REGULAR:
            thisAmount += 2
            if rental.daysRented > 2 {
             thisAmount += (float64(rental.daysRented) - 2) * 1.5
            }
           case NEW_RELEASE:
            thisAmount += float64(rental.daysRented) * 3
           case CHILDRENS:
            thisAmount += 1.5
            if rental.daysRented > 3 {
             thisAmount += (float64(rental.daysRented) - 3) * 1.5
            }
           }
           return thisAmount
          }

          func calcFrequentRenterPoints(rental Rental) int {
           frequentRenterPoints := 1
           switch rental.movie.priceCode {
           case NEW_RELEASE:
            if rental.daysRented > 1 {
             frequentRenterPointst++
            }
           }
           return frequentRenterPoints
          }

          按層分解(Layer Decomposition):

          // bad
          func findSphericalClosest(lat float64, lng float64, locations []Location) *Location {
           var closest *Location
            closestDistance := math.MaxFloat64
            for _, location := range locations {
              latRad := radians(lat)
              lngRad := radians(lng)
              lng2Rad := radians(location.Lat)
              lng2Rad := radians(location.Lng)
              var dist = math.Acos(math.Sin(latRad) * math.Sin(lat2Rad) +  
                                   math.Cos(latRad) * math.Cos(lat2Rad) *
                                   math.Cos(lng2Rad - lngRad) 
                                  )
              if dist < closestDistance {
             closest = &location
                closestDistance = dist
              }
            }
           return closet
          }
          // good
          type Location struct {
          }

          type compare func(left Location, right Location) int

          func min(objects []Location, compare compare) *Location {
           var min *Location
           for _, object := range objects {
            if min == nil {
             min = &object
             continue
            }
            if compare(object, *min) < 0 {
             min = &object
            }
           }
           return min
          }

          func findSphericalClosest(lat float64, lng float64, locations []Location) *Location {
           isCloser := func(left Location, right Location) int {
            leftDistance := rand.Int()
            rightDistance := rand.Int()
            if leftDistance < rightDistance {
             return -1
            } else {
             return 0
            }
           }
           closet := min(locations, isCloser)
           return closet
          }
          注釋

          注釋不應(yīng)重復(fù)代碼的工作。應(yīng)該去解釋代碼的模型和心智模型的映射關(guān)系,應(yīng)說明為什么要使用這個(gè)代碼模型,下面的例子就是反面教材:

          // bad
          /** the name. */
          var name string
          /** the version. */
          var Version string
          /** the info. */
          var info string

          // Find the Node in the given subtree, with the given name, using the given depth.
          func FindNodeInSubtree(subTree *Node, name string, depth *int) *Node {
          }

          下面的例子是正面教材:

          // Impose a reasonable limit - no human can read that much anyway
          const MAX_RSS_SUBSCRIPTIONS = 1000

          // Runtime is O(number_tags * average_tag_depth), 
          // so watch out for badly nested inputs.
          func FixBrokenHTML(HTML string) string {
           // ...
          }

          Shorten Process

          Shorten Process的意思是要縮短人腦“編譯代碼”的流程。應(yīng)該避免寫出像小白鼠走迷路一樣又長(zhǎng)又繞的代碼。所謂又長(zhǎng)又繞的代碼表現(xiàn)在,跨表達(dá)式跟蹤、跨多行函數(shù)跟蹤、跨多個(gè)成員函數(shù)跟蹤、跨多個(gè)文件跟蹤、跨多個(gè)編譯單元跟蹤,甚至是跨多個(gè)代碼倉(cāng)庫(kù)跟蹤。

          對(duì)應(yīng)的手段可以有:引入變量、拆分函數(shù)、提早返回、縮小變量作用域,這些方法最終想達(dá)到的目的都是讓大腦喘口氣,不要一口氣跟蹤太久。同樣來看一些具體的例子:

          例子

          下面的代碼,多種復(fù)合條件組合在一起,你看了半天繞暈了可能也沒看出到底什么情況下為true,什么情況為false。

          // bad
          func (rng *Range) overlapsWith(other *Range) bool {
           return (rng.begin >= other.begin && rng.begin < other.end) ||
            (rng.end > other.begin && rng.end <= other.end) ||
            (rng.begin <= other.begin && rng.end >= other.end)
          }

          但是把情況進(jìn)行拆解,每種條件進(jìn)行單獨(dú)處理。這樣邏輯就很清晰了。

          // good
          func (rng *Range) overlapsWith(other *Range) bool {
           if other.end < rng.begin {
            return false // they end before we begin 
           } 
           if other.begin >= rng.end {
            return false // they begin after we end 
           }
            return true // Only possibility left: they overlap
          }

          再來看一個(gè)例子,一開始你寫代碼的時(shí)候,可能只有一個(gè)if ... else...,后來PM讓加一下權(quán)限控制,于是你可以開心的在if里繼續(xù)套一層if,補(bǔ)丁打完,開心收工,于是代碼看起來像這樣:

          // bad 多層縮進(jìn)的問題
          func handleResult(reply *Reply, userResult int, permissionResult int) {
            if userResult == SUCCESS {
              if permissionResult != SUCCESS {
                reply.WriteErrors("error reading permissions")
               reply.Done()
               return
              }
              reply.WriteErrors("")
            } else {
              reply.WriteErrors("User Result")
            }
            reply.Done()
          }

          這種代碼也比較好改,一般反向?qū)慽f條件返回判否邏輯即可:

          // good
          func handleResult(reply *Reply, userResult int, permissionResult int) {
            defer reply.Done()
            if userResult != SUCCESS {
              reply.WriteErrors("User Result")
              return 
            }
            if permissionResult != SUCCESS {
              reply.WriteErrors("error reading permissions")
              return
            }
            reply.WriteErrors("")
          }

          這個(gè)例子的代碼問題比較隱晦,它的問題是所有內(nèi)容都放在了MooDriver這個(gè)對(duì)象中。

          // bad
          type MooDriver struct {
           gradient Gradient
            splines []Spline
          }
          func (driver *MooDriver) drive(reason string) {
            driver.saturateGradient()
            driver.reticulateSplines()
            driver.diveForMoog(reason)
          }

          比較好的方法是盡可能減少全局scope,而是使用上下文變量進(jìn)行傳遞。

          // good 
          type ExplicitDriver struct {
            
          }

          // 使用上下文傳遞
          func (driver *MooDriver) drive(reason string) {
            gradient := driver.saturateGradient()
            splines := driver.reticulateSplines(gradient)
            driver.diveForMoog(splines, reason)
          }

          Isolate Process

          人腦缺陷是不擅長(zhǎng)同時(shí)跟蹤多件事情,如果”同時(shí)跟蹤“事物的多個(gè)變化過程,這不符合人腦的構(gòu)造;但是如果把邏輯放在很多地方,這對(duì)大腦也不友好,因?yàn)榇竽X需要”東拼西湊“才能把一塊邏輯看全。所以就有了一句很經(jīng)典的廢話,每個(gè)學(xué)計(jì)算機(jī)的大學(xué)生都聽過。你的代碼要做到高內(nèi)聚,低耦合,這樣就牛逼了!-_-|||,但是你要問說這話的人什么叫高內(nèi)聚,低耦合呢,他可能就得琢磨琢磨了,下面來通過一些例子來琢磨一下。

          首先先來玄學(xué)部分,如果你的代碼寫成下面這樣,可讀性就不會(huì)很高。


          一般情況下,我們可以根據(jù)業(yè)務(wù)場(chǎng)景努力把代碼修改成這樣:

          舉幾個(gè)例子,下面這段代碼非常常見,里面version的含義是用戶端上不同的版本需要做不同的邏輯處理。

          func (query *Query) doQuery() {
            if query.sdQuery != nil {
              query.sdQuery.clearResultSet()
            }
            // version 5.2 control
            if query.sd52 {
              query.sdQuery = sdLoginSession.createQuery(SDQuery.OPEN_FOR_QUERY)
            } else {
              query.sdQuery = sdSession.createQuery(SDQuery.OPEN_FOR_QUERY)
            }
            query.executeQuery()
          }

          這段代碼的問題是由于版本差異多塊代碼流程邏輯Merge在了一起,造成邏輯中間有分叉現(xiàn)象。處理起來也很簡(jiǎn)單,封裝一個(gè)adapter,把版本邏輯抽出一個(gè)interface,然后根據(jù)版本實(shí)現(xiàn)具體的邏輯。

          再來看個(gè)例子,下面代碼中根據(jù)expiry和maturity這樣的產(chǎn)品邏輯不同 也會(huì)造成分叉現(xiàn)象,所以你的代碼會(huì)寫成這樣:

          // bad
          type Loan struct {
           start    time.Time
           expiry   *time.Time
           maturity *time.Time
           rating   int
          }

          func (loan *Loan) duration() float64 {
           if loan.expiry == nil {
            return float64(loan.maturity.Unix()-loan.start.Unix()) / 365 * 24 * float64(time.Hour)
           } else if loan.maturity == nil {
            return float64(loan.expiry.Unix()-loan.start.Unix()) / 365 * 24 * float64(time.Hour)
           }
           toExpiry := float64(loan.expiry.Unix() - loan.start.Unix())
           fromExpiryToMaturity := float64(loan.maturity.Unix() - loan.expiry.Unix())
           revolverDuration := toExpiry / 365 * 24 * float64(time.Hour)
           termDuration := fromExpiryToMaturity / 365 * 24 * float64(time.Hour)
           return revolverDuration + termDuration
          }

          func (loan *Loan) unusedPercentage() float64 {
           if loan.expiry != nil && loan.maturity != nil {
            if loan.rating > 4 {
             return 0.95
            } else {
             return 0.50
            }
           } else if loan.maturity != nil {
            return 1
           } else if loan.expiry != nil {
            if loan.rating > 4 {
             return 0.75
            } else {
             return 0.25
            }
           }
           panic("invalid loan")
          }

          解決多種產(chǎn)品邏輯的最佳實(shí)踐是Strategy pattern,代碼如下圖,根據(jù)產(chǎn)品類型創(chuàng)建出不同的策略接口,然后分別實(shí)現(xiàn)duration和unusedPercentage這兩個(gè)方法即可。

          // good
          type LoanApplication struct {
           expiry   *time.Time
           maturity *time.Time
          }

          type CapitalStrategy interface {
           duration() float64
           unusedPercentage() float64
          }

          func createLoanStrategy(loanApplication LoanApplication) CapitalStrategy {
           if loanApplication.expiry != nil && loanApplication.maturity != nil {
            return createRCTL(loanApplication)
           }
           if loanApplication.expiry != nil {
            return createRevolver(loanApplication)
           }
           if loanApplication.maturity != nil {
            return createTermLoan
           }
           panic("invalid loan application")
          }

          但是現(xiàn)實(shí)情況沒有這么簡(jiǎn)單,因?yàn)椴煌挛镌谀阊壑芯褪嵌噙M(jìn)程多線程運(yùn)行的,比如上面產(chǎn)品邏輯的例子,雖然通過一些設(shè)計(jì)模式把執(zhí)行的邏輯隔離到了不同地方,但是代碼中只要含有多種產(chǎn)品,代碼在執(zhí)行時(shí)還是會(huì)有一個(gè)產(chǎn)品選擇的過程。邏輯發(fā)生在同一時(shí)間、同一空間,所以“自然而然”就需要寫在了一起:

          • 功能展示時(shí),由于需要展示多種信息,會(huì)造成 concurrent process

          • 寫代碼時(shí),業(yè)務(wù)包括功能性和非功能性需求,也包括正常邏輯和異常邏輯處理

          • 考慮運(yùn)行效率時(shí),為提高效率我們會(huì)考慮異步I/O、多線程/協(xié)程

          • 考慮流程復(fù)用時(shí),由于版本差異和產(chǎn)品策略也會(huì)造成merged concurrent process

          對(duì)于多種功能雜糅在一起,比如上面的RenderPage函數(shù),對(duì)應(yīng)解法為不要把所有事情合在一起搞,把單塊功能內(nèi)聚,整體再耦合成為一個(gè)單元。

          對(duì)于多個(gè)同步進(jìn)行的I/O操作,可以通過協(xié)程把揉在一起的過程分開來:

          // bad 兩個(gè)I/O寫到一起了
          func sendToPlatforms() {
           httpSend("bloomberg"func(err error) {
            if err == nil {
             increaseCounter("bloomberg_sent"func(err error) {
              if err != nil {
               log("failed to record counter", err)
              }
             })
            } else {
             log("failed to send to bloom berg", err)
            }
           })
           ftpSend("reuters"func(err error) {
            if err == DIRECTORY_NOT_FOUND {
             httpSend("reuterHelp", err)
            }
           })
          }

          對(duì)于這種并發(fā)的I/O場(chǎng)景,最佳解法就是給每個(gè)功能各自寫一個(gè)計(jì)算函數(shù),代碼真正運(yùn)行的時(shí)候是”同時(shí)“在運(yùn)行,但是代碼中是分開的。

          //good 協(xié)程寫法
          func sendToPlatforms() {
           go sendToBloomberg()
           go sendToReuters()
          }

          func sendToBloomberg() {
           err := httpSend("bloomberg")
           if err != nil {
            log("failed to send to bloom berg", err)
            return
           }
           err := increaseCounter("bloomberg_sent")
           if err != nil {
            log("failed to record counter", err)
           }
          }

          func sendToReuters() {
           err := ftpSend("reuters")
           if err == nil {
            httpSend("reutersHelp", err)
           }
          }

          有時(shí),邏輯必須要合并到一個(gè)Process里面,比如在買賣商品時(shí)必須要對(duì)參數(shù)做邏輯檢查:

          // bad
          func buyProduct(req *http.Request) error {
           err := checkAuth(req)
           if err != nil {
            return err
           }
           // ...
          }

          func sellProduct(req *http.Request) error {
           err := checkAuth(req)
           if err != nil {
            return err
           }
           // ...
          }

          這種頭部有公共邏輯經(jīng)典解法是寫個(gè)Decorator單獨(dú)處理權(quán)限校驗(yàn)邏輯,然后wrapper一下正式邏輯即可:

          // good 裝飾器寫法
          func init() {
           buyProduct = checkAuthDecorator(buyProduct)
           sellProduct = checkAuthDecorator(sellProduct)
          }

          func checkAuthDecorator(f func(req *http.Request) errorfunc(req *http.Request) error {
           return func(req *http.Request) error {
            err := checkAuth(req)
            if err != nil {
             return err
            }
            return f(req)
           }
          }

          var buyProduct = func(req *http.Request) error {
           // ...
          }

          var sellProduct = func(req *http.Request) error {
           // ...
          }

          此時(shí)你的代碼會(huì)像這樣:

          當(dāng)然公共邏輯不僅僅存在于頭部,仔細(xì)思考一下所謂的strategy、Template pattern,他們是在邏輯的其他地方去做這樣的邏輯處理。

          這塊有一個(gè)新的概念叫:信噪比。信噪比是一個(gè)相對(duì)概念,信息,對(duì)有用的;噪音,對(duì)沒用的。代碼應(yīng)把什么邏輯寫在一起,不僅取決于讀者是誰(shuí),還取決于這個(gè)讀者當(dāng)時(shí)希望完成什么目標(biāo)。

          比如下面這段C++和Python代碼:

          void sendMessage(const Message &msg) const {...}
          def sendMessage(msg):

          如果你現(xiàn)在要做業(yè)務(wù)開發(fā),你可能會(huì)覺得Python代碼讀起來很簡(jiǎn)潔;但是如果你現(xiàn)在要做一些性能優(yōu)化的工作,C++代碼顯然能給你帶來更多信息。

          再比如下面這段代碼,從業(yè)務(wù)邏輯上講,這段開發(fā)看起來非常清晰,就是去遍歷書本獲取Publisher。

          for _, book := range books {
            book.getPublisher()
          }

          但是如果你看了線上打了如下的SQL日志,你懵逼了,心想這個(gè)OOM真**,真就是一行一行執(zhí)行SQL,這行代碼可能會(huì)引起DB報(bào)警,讓你的DBA同事半夜起來修D(zhuǎn)B。

          SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id
          SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id
          SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id
          SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id
          SELECT * FROM Pubisher WHERE PublisherId = book.publisher_id

          所以如果代碼改成這樣,你可能就會(huì)更加明白這塊代碼其實(shí)是在循環(huán)調(diào)用實(shí)體。

          for _, book := range books {
            loadEntity("publisher", book.publisher_id)
          }

          總結(jié)一下:

          • 優(yōu)先嘗試給每 個(gè)Process一個(gè)自己的函數(shù),不要合并到一起來算
            • 嘗試界面拆成組件
            • 嘗試把訂單拆成多個(gè)單據(jù),獨(dú)立跟蹤多個(gè)流程
            • 嘗試用協(xié)程而不是回調(diào)來表達(dá)concurrent i/o
          • 如果不得不在一個(gè)Process中處理多個(gè)相對(duì)獨(dú)立的事情
            • 嘗試復(fù)制一份代碼,而不是復(fù)用同一個(gè)Process
            • 嘗試顯式插入: state/ adapter/ strategy/template/ visitor/ observer
            • 嘗試隱式插入: decorator/aop
            • 提高信噪比是相對(duì)于具體目標(biāo)的,提高了一個(gè)目標(biāo)的信噪比,就降低了另外一個(gè)目標(biāo)的信噪比

          3.總結(jié)

          當(dāng)我們吐槽這塊代碼可讀性太差時(shí),不要把可讀性差的原因簡(jiǎn)單歸結(jié)為注釋不夠 或者不OO,而是可以從人腦特性出發(fā),根據(jù)下面的圖片去找到代碼問題,然后試著改進(jìn)它(跑了幾年的老代碼還是算了,別改一行線上全炸了: )



          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲?。贿€可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

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

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青青草国产成人AV片免费 | 欧美亚洲成人电影 | 美女裸体无遮挡天天摸天天做 | 蜜臀日韩免费 | 日本成人在线不卡视频 |