1. 程式人生 > >重構程式碼,你真的準備好了嗎

重構程式碼,你真的準備好了嗎

我相信每個接受過老專案的程式設計師可能都吐槽過“前人的程式碼都是屎”。一個已經有些年頭的專案,幾乎肯定可以看到——到處拷貝來拷貝去的程式碼,隨處可見的拼寫錯誤,頭重腳輕的函式……再看一看當年的提交者,可能是公司裡的元老,甚至是大boss,不禁心裡暗暗的鄙視,懷疑是否自己進錯了公司。

而你被分配到接管這坨“屎”一般的程式碼,並且要在上面新增更多的功能,每次的增刪程式碼都讓你如履薄冰,每次遇到原來程式碼裡的bug都讓你的髮際線再次上揚。

終於有一天,你忍不住了,腦子裡面滿滿都是一個念頭——我要重寫這個程式碼。然後你真的這麼做了,花了整整一個晚上/天/星期的時間,把程式碼改成了你心中滿意的模樣。然後程式碼上線了:

Happy Ending:重構的程式碼獲得了同事的交口稱讚,大家紛紛誇你程式碼比以前好寫多了。

Normal Ending:過了幾個月,你發現重構的程式碼又不行了,加一個新功能費死勁了,於是你又在籌劃下一次重構。

Bad Ending:重構的程式碼上線後,bug不斷,老闆奪命連環call讓你連夜修補,你發現老程式碼這麼寫不是沒有道理的。

這樣的故事每個經手過老專案的程式設計師可能都多少有類似的體會,在我的職業生涯中也經歷了屈指可數的幾次重構,然而每一次的重構經歷幾乎都踩到了各種各樣的坑。

你真的需要重構嗎

在重構專案之前,一定要再三的問自己(和自己的組員)這個問題:我們真的需要重構嗎?

重構專案,在只是重構的前提下,對於公司的收益來說是——0,因為你的產品的使用者,他們並不會為你的重構行為來買賬,對於他們來說,你的原始碼寫的好看與否根本無所謂,對他們重要的是產品本身有沒有改進。對於公司來說,重構行為不但沒有帶來任何利益,反而消耗了程式設計師資源,對於公司來說是損失。

一個網際網路產品的生命週期可能就只有短短的幾年,放長一點看,現在寫的程式碼可能過幾年就會毫無用處,在這樣的前提下,現有專案的重構,一定是建立在專案本身還十分有前景的基礎上,這個專案將來還有多少潛力,值不值得去重構?如果這個產品本身並沒有什麼可做的了,那麼是否還值得花時間去重構它?

為什麼需要進行專案重構

每個專案重構的理由各不相同,但個人總結來主要是以下兩點

  1. 原來的專案漏洞太多,或者穩定性太差,當前的框架很難徹底根治。
  2. 新的專案需求,原有的程式框架已經無法滿足。

假設你的專案沒有很多bug,穩定性也很好,或者暫時沒有在現有框架下很難實現的新的需求,那麼不建議進行專案重構。

我在上一家公司的SEM組工作時,經歷的第一次重構,是將後臺的競價計算出的競價的結果,由資料庫的表(Table)儲存改成了推送到佇列系統(RabbitMQ)。後臺競價程式算出的競價結果需要由另一個上傳程式上傳到Adwords等競價平臺,我們在過去的做法是在資料庫建立了一張表,競價程式將算出的新競價儲存在其中,上傳程式則定期的去查詢表中的新加入的記錄,將其成批上傳,並在上傳後刪除。那麼為何要進行這次重構?

  1. 隨著公司的投放的廣告詞增加,單一上傳程式例項很難在短時間內上傳所有的競價,但是如果執行多個上傳程式的例項,則會出現多個示例同時查詢新加入競價並上傳,刪除同一記錄造成資料庫死鎖。(1. 原來的專案漏洞太多,或者穩定性太差,當前的框架很難徹底根治。)
  2. 新業務需求需要計算另一種格式的競價,如果繼續使用資料庫表來儲存,則要麼需要對已有的表進行欄位擴容/修改,要麼建立新的表單。但是當時已經預見將來可能會支援更多格式的競價,於是資料庫表的儲存方式將不再靈活。(2. 新的專案需求,原有的程式框架已經無法滿足。)

重構專案

經過再三衡量,我們終於還是決定重構專案,恭喜你,你將有一段踩坑之旅。

重構專案之前

重構專案的第一步是要了解專案。

重構時最容易發生的一類錯誤是沒有能夠完全的將原來的功能忠實的重現出來。很多開發者並不是手頭的專案的原作者,並且專案也經過了很長時間的迭代,當代碼越滾越大的時候,幾乎沒有人(包括原作者和產品經理)能夠完全瞭解專案到底包含了哪些內容。當你看到重構後的功能和原來一模一樣,並且測試人員也沒有測出問題的時候,說不定哪個猴年馬月新增進來的特殊功能,悄悄的被你幹掉了。等到上線後,這個特殊功能的使用者突然發現功能沒了,於是過來投訴。

重構ING —— 測試

如果說什麼是重構中最重要的第一步, 我認為是測試。

如果原來的程式碼沒有單元測試、整合測試,有條件的話一定要補充上。為什麼測試如此重要?打一個比好,重構就好像對著一把老鑰匙來配新鑰匙,而測試程式碼則是老鑰匙的模子,我們做出來的新鑰匙要能夠和這個模子全對上。這個模子越詳細,則新鑰匙可以正常開鎖的概率越大。

回想我在過去的重構中出現的一次重大失誤,便是在重構過程中,有一個原來的單元測試出現了錯誤,原本的斷言是結果為NULL,但是我的結果是0,當時覺得可能兩種結果都可以,於是錯誤的選擇了將單元測試的結果“改正”,結果在程式碼上線後,0的結果造成了程式輸出和之前相比大不相同。總結一下:1. 如果有整合測試,則這樣的錯誤可以在上線之前發現。2. 應該相信原來的單元測試集,而不應該“想當然”的去認為自己重構的邏輯正確。

重構ING —— 分支

程式碼重構的過程中,一定不建議先刪除程式碼全部重寫。比較推薦的是先拷貝出一個新的函式/檔案/資料夾,然後寫全新的程式碼。為什麼要這麼做?

  1. 在寫新程式碼的時候可以一邊寫一邊參照原來的程式碼。
  2. 新程式碼的程式碼審查(Code Review)會比較乾淨。
  3. 專案管理工具(Git,SVN)的歷史比較乾淨。

回到我上面說的由資料庫的表(Table)儲存改成了推送到佇列系統(RabbitMQ)的重構,當時我的做法是,在競價程式端,重新實現了輸出的函式,使得競價結果可以改為推送到佇列系統。而在上傳程式端,則重新實現了一個新的程式,只從訊息佇列中消費推送的訊息,然後上傳到Adwords等廣告平臺。原有的舊上傳程式則沒有改動絲毫。

重構專案的上線 —— 開關

稍微大一些的重構,我會比較推薦使用程式開關,使用一些控制引數來控制邏輯入口是用老程式碼還是新程式碼,這樣在線上出現了問題,可以及時的調整控制引數,迅速的回滾到老的邏輯。

如果程式執行的結果本身就是不確定的,不容易看出重構的錯誤,甚至推薦在重構的入口處設定A/B測試,這樣在線上讓一部分流量先走重構後的邏輯,同時將新/老邏輯的流量標記成不同的測試bucket,可以在資料測量平臺上看到新老程式碼的表現如何。如果新程式碼的表現合理,則可以不斷加大新程式碼的流量覆蓋,直到100%。

在我上面提到的重構中,我選擇在競價程式計算段建立了一個新的A/B測試,對照組採用將競價結果寫到資料庫的方法,實驗組則將競價結果傳送到訊息佇列。同時在生產環境中,舊的和新的上傳程式都在同時執行。在剛上線的時候,我選擇將1%的競價結果推送到訊息佇列中,然後觀察新的上傳程式能否將訊息佇列中的訊息消耗掉。同時,在產品的監視頁面,對對照組和實驗組的競價結果進行分析,確認兩個組的競價結果並沒有明顯的差別。

總結

總結一下個人的重構心得,重構前是否必要,重構中做好測試、分支、開關