如何寫出來(lái)讓人可以讀懂的代碼
“本文整理自 taowen 師傅在滴滴內(nèi)部的分享。
1.Why
對(duì)一線開發(fā)人員來(lái)說(shuō),每天工作內(nèi)容大多是在已有項(xiàng)目的基礎(chǔ)上繼續(xù)堆代碼。當(dāng)項(xiàng)目實(shí)在堆不動(dòng)時(shí)就需要尋找收益來(lái)重構(gòu)代碼。既然我們的大多數(shù)時(shí)間都花在坐在顯示器前讀寫代碼這件事上,那可讀性不好的代碼都是在謀殺自己or同事的生命,所以不如一開始就提煉技巧,努力寫好代碼; )
2.How
為提高代碼可讀性,先來(lái)分析代碼實(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è)角度來(lái)說(shuō),要提高代碼的可讀性首先需要知道大腦的運(yùn)行機(jī)制。

下面來(lái)看一下人腦適合做的事情和不適合做的事情:
大腦擅長(zhǎng)做的事情
| 名稱 | 圖片 | 說(shuō)明 |
|---|---|---|
| 對(duì)象識(shí)別 |
| 不同于機(jī)器學(xué)習(xí)看無(wú)數(shù)張貓片之后可能還是不能準(zhǔn)確識(shí)別貓這個(gè)對(duì)象,人腦在看過(guò)幾只貓之后就可以很好的識(shí)別。 |
| 空間分解 |
| 人腦不需要標(biāo)注,可以直觀感受到空間中的不同物體。 |
| 時(shí)序預(yù)測(cè) |
| 你的第一感覺(jué)是不是這個(gè)哥們要被車撞了? |
| 時(shí)序記憶 |
| 作為人類生存本能之一,我們多次走過(guò)某個(gè)地方時(shí),人腦會(huì)對(duì)這個(gè)地方形成記憶。 |
| 類比推測(cè) |
| 人腦還有類比功能,比如說(shuō)這道題大多數(shù)人會(huì)選擇C吧。 |
大腦不擅長(zhǎng)做的事情
| 名稱 | 圖片 | 例子 |
|---|---|---|
| 無(wú)法映射到現(xiàn)實(shí)生活經(jīng)驗(yàn)的抽象概念 |
| 人腦看到左圖時(shí),會(huì)比較輕松想到通關(guān)方式,但是如果換成右圖這種抽象的概念,里面的對(duì)象換成了嘿嘿的像素,我們就不知道這是什么鬼了。比如說(shuō)代碼里如果充斥著Z,X,C,V 這樣的變量名,你可能就看懵了。 |
| 冗長(zhǎng)的偵探推理 |
| 這種需要遞歸(or循環(huán))去檢查所有可能性最后找到解法的場(chǎng)景,人腦同樣不擅長(zhǎng)。 |
| 跟蹤多個(gè)同時(shí)變化的過(guò)程 |
| 大腦是個(gè)單線程的CPU,不擅長(zhǎng)左手畫圓,右手畫圈。 |
代碼優(yōu)化理論
了解人腦的優(yōu)缺點(diǎn)后,寫代碼時(shí)就可以根據(jù)人腦的特點(diǎn)對(duì)應(yīng)改善代碼的可讀性了。這里提取出三種理論:
Align Models ,匹配模型:代碼中的數(shù)據(jù)和算法模型 應(yīng)和人腦中的 心智模型對(duì)應(yīng)
Shorten Process , 簡(jiǎn)短處理:寫代碼時(shí)應(yīng) 縮短 “福爾摩斯探案集” 的流程長(zhǎng)度,即不要寫大段代碼
Isolate Process,隔離處理:寫代碼一個(gè)流程一個(gè)流程來(lái)處理,不要同時(shí)描述多個(gè)流程的演進(jìn)過(guò)程
下面通過(guò)例子詳細(xì)解釋這三種模型:
Align Models
在代碼中,模型無(wú)外乎就是數(shù)據(jù)結(jié)構(gòu)與算法,而在人腦中,對(duì)應(yīng)的是心智模型,所謂心智模型就是人腦對(duì)于一個(gè)物體 or 一件事情的想法,我們平時(shí)說(shuō)話就是心智模型的外在表現(xiàn)。寫代碼時(shí)應(yīng)把代碼中的名詞與現(xiàn)實(shí)名詞對(duì)應(yīng)起來(lái),減少人腦從需求文檔到代碼的映射成本。比如對(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í)際含義,沒(méi)必要隨便寫個(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 { // 說(shuō)明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è)過(guò)程
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)過(guò)程
return request.Form.Get("name")
}
func pageUrl(request *http.Request) interface{} { // URL 相關(guān)過(guò)程
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)過(guò)程
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', 10, 64) + "\n"
}
result += "Amount owed is " +
strconv.FormatFloat(bill.totalAmount, 'g', 10, 64) + "\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)說(shuō)明為什么要使用這個(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á)到的目的都是讓大腦喘口氣,不要一口氣跟蹤太久。同樣來(lái)看一些具體的例子:
例子
下面的代碼,多種復(fù)合條件組合在一起,你看了半天繞暈了可能也沒(méi)看出到底什么情況下為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
}
再來(lái)看一個(gè)例子,一開始你寫代碼的時(shí)候,可能只有一個(gè)if ... else...,后來(lái)PM讓加一下權(quán)限控制,于是你可以開心的在if里繼續(xù)套一層if,補(bǔ)丁打完,開心收工,于是代碼看起來(lái)像這樣:
// bad 多層縮進(jìn)的問(wè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è)例子的代碼問(wèn)題比較隱晦,它的問(wèn)題是所有內(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è)變化過(guò)程,這不符合人腦的構(gòu)造;但是如果把邏輯放在很多地方,這對(duì)大腦也不友好,因?yàn)榇竽X需要”東拼西湊“才能把一塊邏輯看全。所以就有了一句很經(jīng)典的廢話,每個(gè)學(xué)計(jì)算機(jī)的大學(xué)生都聽過(guò)。你的代碼要做到高內(nèi)聚,低耦合,這樣就牛逼了!-_-|||,但是你要問(wèn)說(shuō)這話的人什么叫高內(nèi)聚,低耦合呢,他可能就得琢磨琢磨了,下面來(lái)通過(guò)一些例子來(lái)琢磨一下。
首先先來(lá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()
}
這段代碼的問(wèn)題是由于版本差異多塊代碼流程邏輯Merge在了一起,造成邏輯中間有分叉現(xiàn)象。處理起來(lái)也很簡(jiǎn)單,封裝一個(gè)adapter,把版本邏輯抽出一個(gè)interface,然后根據(jù)版本實(shí)現(xiàn)具體的邏輯。
再來(lái)看個(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í)情況沒(méi)有這么簡(jiǎn)單,因?yàn)椴煌挛镌谀阊壑芯褪嵌噙M(jìn)程多線程運(yùn)行的,比如上面產(chǎn)品邏輯的例子,雖然通過(guò)一些設(shè)計(jì)模式把執(zhí)行的邏輯隔離到了不同地方,但是代碼中只要含有多種產(chǎn)品,代碼在執(zhí)行時(shí)還是會(huì)有一個(gè)產(chǎn)品選擇的過(guò)程。邏輯發(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操作,可以通過(guò)協(xié)程把揉在一起的過(guò)程分開來(lái):
// 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) error) func(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ì)我沒(méi)用的。代碼應(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ì)覺(jué)得Python代碼讀起來(lái)很簡(jiǎn)潔;但是如果你現(xiàn)在要做一些性能優(yōu)化的工作,C++代碼顯然能給你帶來(lái)更多信息。
再比如下面這段代碼,從業(yè)務(wù)邏輯上講,這段開發(fā)看起來(lái)非常清晰,就是去遍歷書本獲取Publisher。
for _, book := range books {
book.getPublisher()
}
但是如果你看了線上打了如下的SQL日志,你懵逼了,心想這個(gè)OOM真**,真就是一行一行執(zhí)行SQL,這行代碼可能會(huì)引起DB報(bào)警,讓你的DBA同事半夜起來(lái)修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ù),不要合并到一起來(lái)算 嘗試界面拆成組件 嘗試把訂單拆成多個(gè)單據(jù),獨(dú)立跟蹤多個(gè)流程 嘗試用協(xié)程而不是回調(diào)來(lái)表達(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ù)下面的圖片去找到代碼問(wèn)題,然后試著改進(jìn)它(跑了幾年的老代碼還是算了,別改一行線上全炸了: )

歡迎加我的個(gè)人微信:709834997 備注: From Panda
掃描二維碼關(guān)注此微信號(hào),與一根有態(tài)度的薯?xiàng)l做交流,每篇文章都會(huì)帶給你驚喜。








