下面介紹了幾種優化函數:
1. Extract Method (提煉函數)
解釋:
如果發現一個函數的代碼很長, 很可能的一種情況是這個函數做了很多事情, 找找看函數中有沒有注釋, 往往注釋都是為了解釋下面一塊代碼做的什麼事情, 可以考慮將這塊代碼提煉(Extract)成一個獨立的函數.
這樣做的好處不言而喻, 是面向對象五大基本原則中的單一職責原則 (Single Responsibility Principle), 比較長的函數被拆分成一個個小函數, 將有利於代碼被復用.
沖動前:
- public void Print(Employee employee)
- {
- //print employee's information
- Console.WriteLine("Name:" + employee.Name);
- Console.WriteLine("Sex:" + employee.Sex);
- Console.WriteLine("Age:" + employee.Age);
- //print employee's salary
- Console.WriteLine("Salary:" + employee.Salary);
- Console.WriteLine("Bonus:" + employee.Bonus);
- }
沖動後:
- public void Print(Employee employee)
- {
- //print employee's information
- PrintInfo(employee);
- //print employee's salary
- PrintSalary(employee);
- }
- public void PrintInfo(Employee employee)
- {
- Console.WriteLine("Name:" + employee.Name);
- Console.WriteLine("Sex:" + employee.Sex);
- Console.WriteLine("Age:" + employee.Age);
- }
- public void PrintSalary(Employee employee)
- {
- Console.WriteLine("Salary:" + employee.Salary);
- Console.WriteLine("Bonus:" + employee.Bonus);
- }
2. Inline Method (將函數內聯)
解釋:
有些函數很短, 只有一兩行, 而且代碼的意圖也非常明顯, 這時可以考慮將這個函數干掉, 直接使用函數中的代碼.物件中過多的方法會讓人感到不舒服, 干掉完全不必要的函數後代碼會更簡潔.
沖動前:
- public bool IsDeserving(int score)
- {
- return IsScoreMoreThanSixty(score);
- }
- public bool IsScoreMoreThanSixty(int score)
- {
- return (score > 60);
- }
沖動後:
- public bool IsDeserving(int score)
- {
- return (score > 60) ;
- }
3. Inline Temp (將臨時變量內聯)
解釋:
如果有一個臨時變量 (Temp)用來表示某個函數的返回值, 一般來說, 這樣的做法挺好的. 但如果這個臨時變量實在多余, 將這個臨時變量內聯之後毫不影響代碼的閱讀, 甚至這個臨時變量妨礙了其它重構工作, 就應該將這個臨時變量內聯化.
把這個臨時變量干掉的好處在於減少了函數的長度, 有時可以讓其它重構工作更順利的進行.
沖動前:
- int salary = employee.Salary;
- return (salary > 10000);
沖動後:
- return (employee.Salary > 10000);
- Replace Temp With Query (用查詢式代替臨時變量)
解釋:
程序中有一個臨時變量(Temp)用來保存某個表達式的計算結果, 將這個計算表達式提煉(Extract)到一個獨立的函數(即查詢式Query)中, 將這個臨時變量所有被調用的地方換成對新函數(Query)的調用, 新函數還可以被其它函數使用.
好處在於減少函數長度, 增加代碼復用率, 有利於代碼進一步的重構. 並且注意 Replace Temp With Query 往往是 Extract Method 之前必不可少的步驟, 因為局部變量會使代碼不太容易被提煉, 所以在進行類似的重構前可以將它們替換成查詢式.
下面的這個例子不是很有必要使用Replace Temp With Query, 主要展示如何 Replace Temp With Query. 試想"沖動前"函數中有很多個代碼塊都使用到 totalPrice, 突然有一天我發現這個函數太長, 我需要將這一塊塊的代碼提煉成單獨的函數, 這樣就需要將 totalPrice = price * num; 放到每一個提煉出來的函數中. 而如果原來函數中使用的是查詢式, 就不存在這個問題. 如果查詢式中的計算量很大, 也不建議使用 Replace Temp With Query.
沖動前:
- public double FinalPrice(double price, int num)
- {
- double totalPrice = price * num;
- if (totalPrice > 100)
- return totalPrice * 0.8;
- else
- return totalPrice * 0.9;
- }
沖動後:
- public double FinalPrice(double price, int num)
- {
- if (TotalPrice(price, num) > 100)
- return TotalPrice(price, num) * 0.8;
- else
- return TotalPrice(price, num) * 0.9;
- }
- public double TotalPrice(double price, int num)
- {
- return price * num;
- }
5. Introduce Explaining Variable (引入可以理解的變量)
解釋:
很多時候在條件邏輯表達式中, 很多條件令人難以理解它的意義, 為什麼要滿足這個條件? 不清楚. 可以使用Introduce Explaining Variable將每個條件子句提煉出來, 分別用一個恰當的臨時變量名表示條件子句的意義.
好處在於增加了程序的可讀性.
沖動前:
- if((operateSystem.Contains("Windows"))&& (browser.Contatins("IE")))
- {
- //do something
- }
沖動後:
- bool isWindowsOS = operateSystem.Contains("Windows");
- bool isIEBrowser = browser.Contatins("IE");
- if (isWindowsOS && isIEBrowser)
- {
- //do something
- }
6. Split Temporary Variable (撇清臨時變量)
解釋:
例如代碼中有個臨時變量在函數上面某處表示長方形周長, 在函數下面被賦予面積, 也就是這個臨時變量被賦值超過一次, 且表示的不是同一種量. 應該針對每次賦值, 分配一個獨立的臨時變量.
一個變量只應表示一種量, 否則會令代碼閱讀者感到迷惑.
沖動前:
- double temp = (width + height) * 2;
- //do something
- temp = width * height;
- //do something
沖動後:
- double perimeter = (width + height) * 2;
- //do something
- double area = width * height;
- //do something
7. Remove Assignments to Parameters (消除對參數的賦值操作)
解釋:
傳入參數分"傳值"和"傳址"兩種, 如果是"傳址", 在函數中改變參數的值無可厚非, 因為我們就是想改變原來的值. 但如果是"傳值", 在代碼中為參數賦值, 就會令人產生疑惑. 所以在函數中應該用一個臨時變量代替這個參數, 然後對這個臨時變量進行其它賦值操作.
沖動前:
- public double FinalPrice(double price, int num)
- {
- price = price * num;
- //other calculation with price
- return price;
- }
沖動後:
- public double FinalPrice(double price, int num)
- {
- double finalPrice = price * num;
- //other calculation with finalPrice
- return finalPrice;
- }
8. Replace Method with Method Object (用函數物件代替函數)
解釋:
沖動的寫下一行行代碼後, 突然發現這個函數變得非常大, 而且由於這個函數包含了很多局部變量, 使得無法使用 Extract Method, 這時 Replace Method with Method Object 就起到了殺手锏的效果. 做法是將這個函數放入一個單獨的物件中, 函數中的臨時變量就變成了這個物件裡的值域 (field).
沖動前:
- class Bill
- {
- public double FinalPrice()
- {
- double primaryPrice;
- double secondaryPrice;
- double teriaryPrice;
- //long computation
- ...
- }
- }
沖動後:
- class Bill
- {
- public double FinalPrice()
- {
- return new PriceCalculator(this).compute();
- }
- }
- class PriceCalculator
- {
- double primaryPrice;
- double secondaryPrice;
- double teriaryPrice;
- public PriceCalculator(Bill bill)
- {
- //initial
- }
- public double compute()
- {
- //computation
- }
- }
9. Substitute Algorithm (替換算法)
解釋:
有這麼一個笑話:
某跨國日化公司, 肥皂生產線存在包裝時可能漏包肥皂的問題, 肯定不能把空的肥皂盒賣給顧客, 於是該公司總裁命令組成了以博士牽頭的專家組對這個問題進行攻關, 該研發團隊使用了世界上最高精尖的技術 (如紅外探測, 激光照射等), 在花費了大量美金和半年的時間後終於完成了肥皂盒檢測系統, 探測到空的肥皂盒以後, 機械手會將空盒推出去. 這一辦法將肥皂盒空填率有效降低至5%以內, 問題基本解決.
而某鄉鎮肥皂企業也遇到類似問題, 老板命令初中畢業的流水線工頭想辦法解決之, 經過半天的思考, 該工頭拿了一台電扇到生產線的末端對著傳送帶猛吹, 那些沒有裝填肥皂的肥皂盒由於重量輕就都被風吹下去了...
這個笑話可以很好的解釋 Substitute Algorithm, 對於函數中復雜的算法, 盡量想辦法將這個算法簡單化, 從而達到與之前同樣甚至更好的效果.
本文鏈接: