從重構到吐血 - 我是如何刪掉 6 萬行程式碼並且不刪減原有功能的

jetleesg發表於2017-12-10

原文發表在 近期重構工作的一點收穫

從重構到吐血 - 我是如何刪掉 6 萬行程式碼並且不刪減原有功能的

以前做個人專案的時候,簡歷上寫過重構了三次,後來在扇貝面試的時候,面試官問三次分別重構了什麼,仔細想想那時候的重構並不算重構,第一次是 UI 改版,但是專案結構沒什麼大的變化,第二次是整體遷移到了 CocoaPods,這次勉強能算重構,第三次僅僅是變數名方法名空行這些地方的風格統一而已。

在現在工作的地方,接手這些專案之後,主要工作做的是重構,而重構工作,本來想寫成一行,結果發現挺多,我列個列表吧:

  • 刪除沒用到的第三方庫
  • 刪除不合理的第三方庫,使用系統自帶的或者自己造輪子
  • 刪除定義好但是沒有用到的變數
  • 刪除 import 進來但是沒有用到的標頭檔案
  • 刪除更舊專案留下來的用不到的邏輯
  • Controller 層不合理的層級結構重構,無用程式碼清理
  • View 層不合理的結構重構
  • Service 層冗餘的寫法重構
  • Model 層不合理的寫法重構
  • 拆開不合理的耦合
  • 耦合一個類別的模組
  • 修復了多處記憶體洩露
  • 修復了多處迴圈引用
  • 優化編譯速度
  • 消除專案中的 warning

關於刪除程式碼,在某個專案裡,Pods 資料夾那些第三方庫的程式碼刪了 9 萬多行(那個目錄沒有被 git ignore 掉),專案裡面刪除了大約 4 萬行,其中大量程式碼是該專案之前的專案裡面留下來的東西,只不過沒人清理。在刪了 4 萬行之後,程式仍然能完整的跑。

接下來是做了部分重構,把一些第三方庫刪掉,自己造輪子,在這個過程中,累計刪除了 1.2 萬行程式碼,增加了 1100 行左右。

整個重構工作下來,編譯速度從 2-3 分鐘減小到了 40 多秒,warning 從 70 多減少到了 0,第三方庫的數量從 51 個減少到了 13 個,安裝包從 22.1M 減小到了 3.7M,功能反而比之前還要多。

記憶體洩露方面,因為沒人在意這件事,有一個功能使用一次,就會增加好幾百 kb 記憶體,那部分程式碼是用 C 寫的,所以及時釋放記憶體,並且優化下呼叫方式,記憶體洩露的問題就完美解決。

迴圈引用方面,是因為有人把 Xcode 的 warning 關了,後來開啟的時候,發現了四個迴圈引用 + 幾十個 warning,並且測試過程中發現那個頁面不斷開啟退出,程式會 crash。

籠統的就這麼多,我再來分享幾個具體的點。

避免濫用單例

單例用著確實爽,但是程式退出之前是不會被回收的,如果是整個生命週期基本用不到的模組做成單例,那麼只會浪費記憶體而已。

避免無用的層級

具體是什麼意思呢,用網路層舉例子,封裝 AFN 是一層,API 的字尾字串放一層,構造請求放一層,OAuth 授權放一層,發普通請求又是一層。增加一個 API,至少要修改 6 個檔案。寫著也很痛苦,看著也很痛苦啊。

網路層就只設計一層,封裝 AFN,發請求的函式也在裡面,API 地址直接用字串寫進去,搞那麼多層沒實際意義,在這麼小的一個專案裡面。

除此之外,關於專案檔案結構,一兩個檔案的建議不要新建資料夾放進去,這個主要是個人習慣,其實無大礙。

合理設計方法名

不留隱患

- (void)requestAtPathForRouteNamed:(NSString *)routeName object:(id)object parameters:(NSDictionary *)parameters
複製程式碼

- (void)requestWithMethod:(XXHTTPMethod)method path:(NSString *)path params:(id)params paramsType:(XXParamType)paramsType
複製程式碼

之前這樣設計的目的是 param 放 form data 型別資料,object 放 json 格式,顯然不合理,同一個 API 不應該允許同時存在 form data 和 json,如果採用第一種,新來的同事可能會認為這兩個都可以填資料,這是不符合我們期望的。

甚至再極端一點,某天我們需要傳檔案過去,是不是還得再擴充欄位。

如果採用第二種,param 是 id 型別,如果是 json,type 傳入 json 列舉型別,如果是二進位制,type 傳入二進位制列舉型別,只留一個欄位暴露給開發者更合理。

避免耦合

我們的專案中有一條漸變顏色的線要到處用到,這條線我們放在了 UIImage+XXUtil.h 裡面,之前的設計是這樣的:

+ (instancetype)xx_navigationBarShadowImage
複製程式碼

在 .m 的實現中,還把 UIColor+XXTheme 耦合進去了,並且這個方法已經脫離了類名 Util 的實質,他已經不是一個通用的工具了,重構之後的命名是這樣的:

+ (instancetype)xx_gradientImageWithStartColor:(UIColor *)aColor endColor:(UIColor *)bColor andWidth:(CGFloat)width
複製程式碼

這樣就很符合 Util 這個 category 名字。

避免濫用繼承

繼承確實很好用,帶來的後果就是子類會把父類的方法挨個執行一遍,乍一看沒什麼,但是如果這個方法很消耗效能呢。

我們這個專案就遇到了,app 經常卡死,用著用著,就 freeze 了,點哪裡都沒反應。因為所有頁面都繼承自基類的一個設計,恰好基類裡面有一個比較耗時的操作,每個頁面都會執行至少三次,就導致了頁面假死。

重構後的做法是設計成一個 category,只是給 UIViewController 新增了幾個方法,按需呼叫,不需要在每個頁面都呼叫,於是解決了這個詭異的 bug。

合理選擇第三方庫

如果有一個功能,迫於各種原因,不得不採用第三方庫,至少也要選一個 GitHub 上 star 比較多的吧,其次是看看 issue 列表有沒有什麼很嚴重的 bug 沒修好,以及相容性問題,多養成好習慣,慢慢就能篩選出來最合適的庫了。

避免濫用第三方庫

我們的專案之前有用到 YYText 這個庫,就為了一段文字裡面加一張圖片,活動當天 iOS9 裝置出現好幾百次 crash,實際上這段程式碼用 NSAttributedString attributedStringWithAttachment 寫一下,七行就夠了,七行替代掉一個不穩定的第三方庫,還是很划算的。

不知道因為什麼原因,可能是更舊的專案裡面用了 PSCollectionView,能跑就沒去重構,這類庫也是屬於完全沒必要的,系統自帶的足夠好用,並且更安全。

各司其職

資料的處理,比如字串進行 UTF8 編碼,時間戳轉成 YYYY-MM-DD 字串,這些都放在 Model 層來處理,各司其職,Model 層就是做資料處理的。

結合實際需求

後端把各種 ID 用 long 型來記錄,是因為他們要做索引,為了索引速度。而客戶端完全沒這個需求,直接用 string 就好,還不用擔心長度不夠的溢位,做展示的時候還不用轉型別。

同樣的,金額按理說應該用雙精度浮點型,因為 float 的精度不夠,結合我的開發經驗看,金額很少要客戶端做加減,直接用 string 即可,需要計算的時候再轉換,只轉換一次,避免丟失精度。

關於命名規範

蘋果的 UIKit 就是最好的例子,寫什麼元件不知道名字怎麼起的時候,就想想蘋果有沒有類似的元件,去找找靈感。

避免無意義的註釋

OC 的方法名本身就很長很清晰了,只是給方法名中間加幾個空格,然後作為註釋,跟沒寫一樣吧。

不用的程式碼刪掉

Git 的作用就是隨時回溯以前版本,程式碼都是能找到的,把程式碼註釋掉,再寫一行類似的,除了增加閱讀成本,容易引起歧義,應該沒什麼用了。一個檔案一共一兩百行,開啟之後發現七八十行程式碼被註釋了,這種感覺相當操蛋,影響閱讀。

重構祖傳程式碼真的會有一種活久見的感覺,上面提到的那些是印象比較深刻的,還有一些小問題已經悄悄解決了,希望我寫的程式碼不會讓後面的同學也這樣認為吧。

相關文章