書中每個章節都推薦了大量的重構手段,當開發者在面對大量無論是已經熟悉的還是新了解的重構手段時,如何快速的回憶并且選擇更高效的方式進行重構? 對于這個問題書中推薦了一種重構記錄方式:
采用從名稱、速寫、動機、做法以及范例五個方面進行記錄。
下面通過文字的方式記錄作者常使用的幾種重構方式:
提煉函數
-
動機
什么時候該提煉函數?
當聲明與實現不同時,這時候導致再次開發時需要通過注釋+代碼邏輯才能明白用途時,需要提煉出函數。舉例: 函數名聲明為highlight(高亮),但是實現邏輯是調用了reverse方法,再setColor:等行為,這樣會導致聲明和實現之間有相當大的距離。
一行代碼的短函數 vs 性能?
有些人會擔心短函數會造成大量函數調用,棧內層級的增加會影響運行的性能,但是現有硬件的支持這種影響其實已經可以忽略;而且短函數常常能讓編譯器的優化功能運轉更良好,因為短函數可以更容易的被緩存,所以不用過早的擔心性能問題
總結:當發現某段代碼需要注釋才能被理解用途時(聲明與實現存在差異),就該主動的提煉函數。
-
做法
1.函數命名: 在提取邏輯上寫上關于本段代碼用途描述的注釋,然后通過注釋翻譯成函數名。無需計較命名不準確的問題,因為函數名不是一蹴而就的,而是隨著理解的不斷加深和上下文的變化,不斷的更新的。
2.參數列表: 提取的代碼邏輯中的參數,如果只有該部分使用那么可直接提取成一個 參數查詢的函數;如果不止該部分代碼中使用,那么可以作為函數入參傳遞進來。
3.上下文中替換并測試: 重構應該是一小步一小步的前進,每次有代碼修改都應該及時編譯測試。
內聯函數
-
動機
經常以簡短的函數表現動作意圖,這樣會使代碼更清晰易讀;但有時候遇到的某些函數,其內部代碼和函數名稱同樣清晰易讀,這時候應該去掉這個函數直接使用其中的代碼;畢竟非必要的間接性使用總是讓人不舒服的(因為會導致開發時閱讀總跳來跳去的!!)
范例
- (void)getRating:(Driver *)driver {
return [self moreThanFiveLateDeliveries:driver] ? 2: 1;
}
- (BOOL)moreThanFiveLateDeliveries:(NSInteger)driver {
return driver.numberOfLateDeliveries > 5;
}
以簡單的例子解釋,moreThanFiveLateDeliveries: 函數名作為中間層并沒有為原來的邏輯增彩,無任何的價值,所以這時候可以直接將函數去掉直接使用實現中的代碼:
- (void)getRating:(Driver *)driver {
return driver.numberOfLateDeliveries > 5 ? 2: 1
}
-
注意
1.當函數在多個位置使用時,在移除時需找到所有的調用點進行替換;
2.移除函數定義的前提是函數內部實現已足夠清晰易讀,能直接表達出 函數名 所表達的意義;在處理一些復雜情況時就不要使用該重構方法!!
提煉變量(Extract Variable)
-
動機
當表達式較復雜難以閱讀時,局部變量名可以幫助更好的理解開發者想表達的意圖;并且作為開發者定義變量名也能在調試時提供更便利的操作(比如 ex (方法名) = (新定義行為)等)。
范例
- (CGFloat)price:(Order *)order {
return order.quantity * order.itemPrice - Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 + Math.min(order.quantity * order.itemPrice * 0.1, 100);
}
在閱讀以上函數內容時,雖然使用的參數較少、命名規范,但是短時間內還是無法明白其中計算邏輯,所以如果將表達式賦值給局部變量:
首先 order.quantity * order.itemPrice = 底價
其次 Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 = 批發折扣
最后 Math.min(order.quantity * order.itemPrice * 0.1, 100) = 運費
通過定義參數名,針對每個變量加一些業務注釋,就能更好的理解這段代碼:
CGFloat basePrice = order.quantity * order.itemPrice
CGFloat quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05
CGFloat freight = Math.min(basePrice * 0.1, 100)
最終代碼呈現:
- (CGFloat)price:(Order *)order {
// 底價
CGFloat basePrice = order.quantity * order.itemPrice;
// 批發折扣
CGFloat quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
// 運費
CGFloat freight = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + freight;
}
內聯變量
-
動機
變量名的定義是為了給表達式提供更具有理解性、更有意義的名字,但是有時候名字反而會妨礙到上下文的代碼,影響理解,這時候就應該消除變量。
范例
NSInteger age = lizhou.age;
這種局部變量毫無價值,應使用該重構方法去除。
改變函數聲明
-
動機
函數名命名方法?
函數名是在理解的不斷加深和上下文的變化中,不斷更新改名以此來更清晰的表達這塊代碼的用途(在開發中一旦發現有更好的命名應該盡快為函數改名!)。
函數的入參如何考慮?
函數的參數列表闡明了函數如何與外部世界共處,也就是我們在定義函數時需要明確函數的上下文,只有在這個上下文中才能使用該函數;由于參數列表是改變連接一個模塊所需的條件,所以需要不斷的去除不必要的耦合。
結論:如何定義正確的函數名?如何定義核實的入參列表?這都是沒有正確答案的,因為答案會隨著時間變化;所以掌握這種重構方法,在對代碼理解加深的過程中一步一步的重構。
范例
@interface TopModel: NSObject
@property (nonatomic, strong) NavModel *nav;
@property (nonatomic, strong) contentModel *content;
@property (nonatomic, strong) BottomModel *bottom;
@end
1.改變參數列表:定義合適的范圍
- (void)bindData:(TopModel *)model {
// 內部只使用了TopModel中的nav 和 bottom中的A個字段
// 從某種意義上來說,可以只傳遞 nav 和 bottom中的 A字段
// 在開發遞進的過程中,當參數不斷增加后可以自定義獨立的Model,但是一次性將頂層的 TopModel 傳遞進來,反而限制了這個類的使用范圍
}
2.當修改某個方法名或參數列表后,應該使用 deprecated 標注,而不是直接刪除。等到版本升級一段時間后再進行移除。
- (void)bindData:(TopModel *)model __attribute__((deprecated));
- (void)bindData:(NavModel *)nav withA:(NSString *)A;
引入參數對象
動機
當一組數據總是結伴出現時,可以通過定義一個新類型的Model進行處理。這項重構真正的意義在于:會讓代碼具有更明確的界限!范例
@interface NavModel: NSObject
@property (nonatomic, strong) NavLeftModel *left;
@property (nonatomic, strong) NavRightModel *right;
@property (nonatomic, strong) NavCenterModel *center;
@end
@interface TopModel: NSObject
@property (nonatomic, strong) NavModel *nav;
@property (nonatomic, strong) contentModel *content;
@property (nonatomic, strong) BottomModel *bottom;
@end
在傳遞時,有多個地方使用AView,傳遞的數據較多但是數據結構不盡相同時,為了提高AView的復用性這時候可以定義AModel來封裝所有需要傳遞的字段。
@interface AModel: NSObject
// 自定義需要的字段
@end
使用這種方法重構會導致更深層次的改變,也就相當于開發者只要在使用該類時就需要將自己持有的數據結構轉換成AModel的格式,當然如果AModel中字段定義存在歧義,那么就達不到想要的效果。所以開發者應盡可能的增加通用化數據結構設計!