程式碼壞味道之程式碼臃腫

靜默虛空發表於2019-03-07

:notebook: 本文已歸檔到:「blog

翻譯自:sourcemaking.com/refactoring…

程式碼臃腫(Bloated)這組壞味道意味著:程式碼中的類、函式、欄位沒有經過合理的組織,只是簡單的堆砌起來。這一型別的問題通常在程式碼的初期並不明顯,但是隨著程式碼規模的增長而逐漸積累(特別是當沒有人努力去根除它們時)。

基本型別偏執

基本型別偏執(Primitive Obsession)

  • 使用基本型別而不是小物件來實現簡單任務(例如貨幣、範圍、電話號碼字串等)。
  • 使用常量編碼資訊(例如一個用於引用管理員許可權的常量USER_ADMIN_ROLE = 1 )。
  • 使用字串常量作為欄位名在陣列中使用。


程式碼壞味道之程式碼臃腫

問題原因

類似其他大部分壞味道,基本型別偏執誕生於類初建的時候。一開始,可能只是不多的欄位,隨著表示的特性越來越多,基本資料型別欄位也越來越多。

基本型別常常被用於表示模型的型別。你有一組數字或字串用來表示某個實體。

還有一個場景:在模擬場景,大量的字串常量被用於陣列的索引。

解決方法


程式碼壞味道之程式碼臃腫

大多數程式語言都支援基本資料型別和結構型別(類、結構體等)。結構型別允許程式設計師將基本資料型別組織起來,以代表某一事物的模型。

基本資料型別可以看成是機構型別的積木塊。當基本資料型別數量成規模後,將它們有組織地結合起來,可以更方便的管理這些資料。

  • 如果你有大量的基本資料型別欄位,就有可能將其中部分存在邏輯聯絡的欄位組織起來,形成一個類。更進一步的是,將與這些資料有關聯的方法也一併移入類中。為了實現這個目標,可以嘗試 以類取代型別碼(Replace Type Code with Class)
  • 如果基本資料型別欄位的值是用於方法的引數,可以使用 引入引數物件(Introduce Parameter Object)保持物件完整(Preserve Whole Object)
  • 如果想要替換的資料值是型別碼,而它並不影響行為,則可以運用 以類取代型別碼(Replace Type Code with Class) 將它替換掉。如果你有與型別碼相關的條件表示式,可運用 以子類取代型別碼(Replace Type Code with Subclass)以狀態/策略模式取代型別碼(Replace Type Code with State/Strategy) 加以處理。
  • 如果你發現自己正從陣列中挑選資料,可運用 以物件取代陣列(Replace Array with Object)

收益

  • 多虧了使用物件替代基本資料型別,使得程式碼變得更加靈活。
  • 程式碼變得更加易讀和更加有組織。特殊資料可以集中進行操作,而不像之前那樣分散。不用再猜測這些陌生的常量的意義以及它們為什麼在陣列中。
  • 更容易發現重複程式碼。


程式碼壞味道之程式碼臃腫

重構方法說明

以類取代型別碼(Replace Type Code with Class)

問題

類之中有一個數值型別碼,但它並不影響類的行為。


程式碼壞味道之程式碼臃腫

解決

以一個新的類替換該數值型別碼。


程式碼壞味道之程式碼臃腫

引入引數物件(Introduce Parameter Object)

問題

某些引數總是很自然地同時出現。


程式碼壞味道之程式碼臃腫

解決

以一個物件來取代這些引數。

程式碼壞味道之程式碼臃腫

保持物件完整(Preserve Whole Object)

問題

你從某個物件中取出若干值,將它們作為某一次函式呼叫時的引數。

int low = daysTempRange.getLow();
int high = daysTempRange.getHigh();
boolean withinPlan = plan.withinRange(low, high);
複製程式碼

解決

改為傳遞整個物件。

boolean withinPlan = plan.withinRange(daysTempRange);
複製程式碼

以子類取代型別碼(Replace Type Code with Subclass)

問題

你有一個不可變的型別碼,它會影響類的行為。


程式碼壞味道之程式碼臃腫

解決

以子類取代這個型別碼。


程式碼壞味道之程式碼臃腫

以狀態/策略模式取代型別碼(Replace Type Code with State/Strategy)

問題

你有一個型別碼,它會影響類的行為,但你無法通過繼承消除它。


程式碼壞味道之程式碼臃腫

解決

以狀態物件取代型別碼。


程式碼壞味道之程式碼臃腫

以物件取代陣列(Replace Array with Object)

問題

你有一個陣列,其中的元素各自代表不同的東西。

String[] row = new String[3];
row[0] = "Liverpool";
row[1] = "15";
複製程式碼

解決

以物件替換陣列。對於陣列中的每個元素,以一個欄位來表示。

Performance row = new Performance();
row.setName("Liverpool");
row.setWins("15");
複製程式碼

資料泥團

資料泥團(Data Clumps)

有時,程式碼的不同部分包含相同的變數組(例如用於連線到資料庫的引數)。這些綁在一起出現的資料應該擁有自己的物件。


程式碼壞味道之程式碼臃腫

問題原因

通常,資料泥團的出現時因為糟糕的程式設計結構或“複製-貼上式程式設計”。

有一個判斷是否是資料泥團的好辦法:刪掉眾多資料中的一項。這麼做,其他資料有沒有因而失去意義?如果它們不再有意義,這就是個明確的訊號:你應該為它們產生一個新的物件。

解決方法

  • 首先找出這些資料以欄位形式出現的地方,運用 提煉類(Extract Class) 將它們提煉到一個獨立物件中。
  • 如果資料泥團在函式的引數列中出現,運用 引入引數物件(Introduce Parameter Object) 將它們組織成一個類。
  • 如果資料泥團的部分資料出現在其他函式中,考慮運用 保持物件完整(Preserve Whole Object) 將整個資料物件傳入到函式中。
  • 檢視一下使用這些欄位的程式碼,也許,將它們移入一個資料類是個不錯的主意。

收益

  • 提高程式碼易讀性和組織性。對於特殊資料的操作,可以集中進行處理,而不像以前那樣分散。
  • 減少程式碼量。


程式碼壞味道之程式碼臃腫

何時忽略

  • 有時為了物件中的部分資料而將整個物件作為引數傳遞給函式,可能會產生讓兩個類之間不收歡迎的依賴關係,這中情況下可以不傳遞整個物件。

重構方法說明

提煉類(Extract Class)

問題

某個類做了不止一件事。


程式碼壞味道之程式碼臃腫

解決

建立一個新類,將相關的欄位和函式從舊類搬移到新類。


程式碼壞味道之程式碼臃腫

引入引數物件(Introduce Parameter Object)

問題

某些引數總是很自然地同時出現。


程式碼壞味道之程式碼臃腫

解決

以一個物件來取代這些引數。


程式碼壞味道之程式碼臃腫

保持物件完整(Preserve Whole Object)

問題

你從某個物件中取出若干值,將它們作為某一次函式呼叫時的引數。

int low = daysTempRange.getLow();
int high = daysTempRange.getHigh();
boolean withinPlan = plan.withinRange(low, high);
複製程式碼

解決

改為傳遞整個物件。

boolean withinPlan = plan.withinRange(daysTempRange);
複製程式碼

過大的類

過大的類(Large Class)

一個類含有過多欄位、函式、程式碼行。


程式碼壞味道之程式碼臃腫

問題原因

類通常一開始很小,但是隨著程式的增長而逐漸膨脹。

類似於過長函式,程式設計師通常覺得在一個現存類中新增新特性比建立一個新的類要容易。

解決方法

設計模式中有一條重要原則:職責單一原則。一個類應該只賦予它一個職責。如果它所承擔的職責太多,就該考慮為它減減負。


程式碼壞味道之程式碼臃腫

  • 如果過大類中的部分行為可以提煉到一個獨立的元件中,可以使用 提煉類(Extract Class)
  • 如果過大類中的部分行為可以用不同方式實現或使用於特殊場景,可以使用 提煉子類(Extract Subclass)
  • 如果有必要為客戶端提供一組操作和行為,可以使用 提煉介面(Extract Interface)
  • 如果你的過大類是個 GUI 類,可能需要把資料和行為移到一個獨立的領域物件去。你可能需要兩邊各保留一些重複資料,並保持兩邊同步。 複製被監視資料(Duplicate Observed Data) 可以告訴你怎麼做。

收益

  • 重構過大的類可以使程式設計師不必記住一個類中大量的屬性。
  • 在大多數情況下,分割過大的類可以避免程式碼和功能的重複。


程式碼壞味道之程式碼臃腫

重構方法說明

提煉類(Extract Class)

問題

某個類做了不止一件事。


程式碼壞味道之程式碼臃腫

解決

建立一個新類,將相關的欄位和函式從舊類搬移到新類。


程式碼壞味道之程式碼臃腫

提煉子類(Extract Subclass)

問題

一個類中有些特性僅用於特定場景。


程式碼壞味道之程式碼臃腫

解決

建立一個子類,並將用於特殊場景的特性置入其中。


程式碼壞味道之程式碼臃腫

提煉介面(Extract Interface)

問題

多個客戶端使用一個類部分相同的函式。另一個場景是兩個類中的部分函式相同。


程式碼壞味道之程式碼臃腫

解決

移動相同的部分函式到介面中。


程式碼壞味道之程式碼臃腫

複製被監視資料(Duplicate Observed Data)

問題

如果儲存在類中的資料是負責 GUI 的。


程式碼壞味道之程式碼臃腫

解決

一個比較好的方法是將負責 GUI 的資料放入一個獨立的類,以確保 GUI 資料與域類之間的連線和同步。


程式碼壞味道之程式碼臃腫

過長函式

過長函式(Long Method)

一個函式含有太多行程式碼。一般來說,任何函式超過 10 行時,你就可以考慮是不是過長了。 函式中的程式碼行數原則上不要超過 100 行。


程式碼壞味道之程式碼臃腫

問題的原因

通常情況下,建立一個新函式的難度要大於新增功能到一個已存在的函式。大部分人都覺得:“我就新增這麼兩行程式碼,為此新建一個函式實在是小題大做了。”於是,張三加兩行,李四加兩行,王五加兩行。。。函式日益龐大,最終爛的像一鍋漿糊,再也沒人能完全看懂了。於是大家就更不敢輕易動這個函式了,只能惡性迴圈的往其中新增程式碼。所以,如果你看到一個超過 200 行的函式,通常都是多個程式設計師東拼西湊出來的。

解決函式

一個很好的技巧是:尋找註釋。新增註釋,一般有這麼幾個原因:程式碼邏輯較為晦澀或複雜;這段程式碼功能相對獨立;特殊處理。 如果程式碼前方有一行註釋,就是在提醒你:可以將這段程式碼替換成一個函式,而且可以在註釋的基礎上給這個函式命名。如果函式有一個描述恰當的名字,就不需要去看內部程式碼究竟是如何實現的。就算只有一行程式碼,如果它需要以註釋來說明,那也值得將它提煉到獨立函式中。


程式碼壞味道之程式碼臃腫

  • 為了給一個函式瘦身,可以使用 提煉函式(Extract Method)
  • 如果區域性變數和引數干擾提煉函式,可以使用 以查詢取代臨時變數(Replace Temp with Query)引入引數物件(Introduce Parameter Object)保持物件完整(Preserve Whole Object)
  • 如果前面兩條沒有幫助,可以通過 以函式物件取代函式(Replace Method with Method Object) 嘗試移動整個函式到一個獨立的物件中。
  • 條件表示式和迴圈常常也是提煉的訊號。對於條件表示式,可以使用 分解條件表示式(Decompose Conditional) 。至於迴圈,應該使用 提煉函式(Extract Method) 將迴圈和其內的程式碼提煉到獨立函式中。

收益

  • 在所有型別的物件導向程式碼中,函式比較短小精悍的類往往生命週期較長。一個函式越長,就越不容易理解和維護。
  • 此外,過長函式中往往含有難以發現的重複程式碼。


程式碼壞味道之程式碼臃腫

效能

是否像許多人說的那樣,增加函式的數量會影響效能?在幾乎絕大多數情況下,這種影響是可以忽略不計,所以不用擔心。 此外,現在有了清晰和易讀的程式碼,在需要的時候,你將更容易找到真正有效的函式來重組程式碼和提高效能。

重構方法說明

提煉函式(Extract Method)

問題

你有一段程式碼可以組織在一起。

void printOwing() {
  printBanner();

  //print details
  System.out.println("name: " + name);
  System.out.println("amount: " + getOutstanding());
}
複製程式碼

解決

移動這段程式碼到一個新的函式中,使用函式的呼叫來替代老程式碼。

void printOwing() {
  printBanner();
  printDetails(getOutstanding());
}

void printDetails(double outstanding) {
  System.out.println("name: " + name);
  System.out.println("amount: " + outstanding);
}
複製程式碼

以查詢取代臨時變數(Replace Temp with Query)

問題

將表示式的結果放在區域性變數中,然後在程式碼中使用。

double calculateTotal() {
  double basePrice = quantity * itemPrice;
  if (basePrice > 1000) {
    return basePrice * 0.95;
  }
  else {
    return basePrice * 0.98;
  }
}
複製程式碼

解決

將整個表示式移動到一個獨立的函式中並返回結果。使用查詢函式來替代使用變數。如果需要,可以在其他函式中合併新函式。

double calculateTotal() {
  if (basePrice() > 1000) {
    return basePrice() * 0.95;
  }
  else {
    return basePrice() * 0.98;
  }
}
double basePrice() {
  return quantity * itemPrice;
}
複製程式碼

引入引數物件(Introduce Parameter Object)

問題

某些引數總是很自然地同時出現。


程式碼壞味道之程式碼臃腫

解決

以一個物件來取代這些引數。


程式碼壞味道之程式碼臃腫

保持物件完整(Preserve Whole Object)

問題

你從某個物件中取出若干值,將它們作為某一次函式呼叫時的引數。

int low = daysTempRange.getLow();
int high = daysTempRange.getHigh();
boolean withinPlan = plan.withinRange(low, high);
複製程式碼

解決

改為傳遞整個物件。

boolean withinPlan = plan.withinRange(daysTempRange);
複製程式碼

以函式物件取代函式(Replace Method with Method Object)

問題

你有一個過長函式,它的區域性變數交織在一起,以致於你無法應用提煉函式(Extract Method) 。

class Order {
  //...
  public double price() {
    double primaryBasePrice;
    double secondaryBasePrice;
    double tertiaryBasePrice;
    // long computation.
    //...
  }
}
複製程式碼

解決

將函式移到一個獨立的類中,使得區域性變數成了這個類的欄位。然後,你可以將函式分割成這個類中的多個函式。

class Order {
  //...
  public double price() {
    return new PriceCalculator(this).compute();
  }
}

class PriceCalculator {
  private double primaryBasePrice;
  private double secondaryBasePrice;
  private double tertiaryBasePrice;

  public PriceCalculator(Order order) {
    // copy relevant information from order object.
    //...
  }

  public double compute() {
    // long computation.
    //...
  }
}
複製程式碼

分解條件表示式(Decompose Conditional)

問題

你有複雜的條件表示式。

if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
  charge = quantity * winterRate + winterServiceCharge;
}
else {
  charge = quantity * summerRate;
}
複製程式碼

解決

根據條件分支將整個條件表示式分解成幾個函式。

if (notSummer(date)) {
  charge = winterCharge(quantity);
}
else {
  charge = summerCharge(quantity);
}
複製程式碼

過長引數列

過長引數列(Long Parameter List)

一個函式有超過 3、4 個入參。


程式碼壞味道之程式碼臃腫

問題原因

過長引數列可能是將多個演算法併到一個函式中時發生的。函式中的入參可以用來控制最終選用哪個演算法去執行。

過長引數列也可能是解耦類之間依賴關係時的副產品。例如,用於建立函式中所需的特定物件的程式碼已從函式移動到呼叫函式的程式碼處,但建立的物件是作為引數傳遞到函式中。因此,原始類不再知道物件之間的關係,並且依賴性也已經減少。但是如果建立的這些物件,每一個都將需要它自己的引數,這意味著過長引數列。

太長的引數列難以理解,太多引數會造成前後不一致、不易使用,而且一旦需要更多資料,就不得不修改它。

解決方案


程式碼壞味道之程式碼臃腫

  • 如果向已有的物件發出一條請求就可以取代一個引數,那麼你應該使用 以函式取代引數(Replace Parameter with Methods) 。在這裡,,“已有的物件”可能是函式所屬類裡的一個欄位,也可能是另一個引數。
  • 你還可以運用 保持物件完整(Preserve Whole Object) 將來自同一物件的一堆資料收集起來,並以該物件替換它們。
  • 如果某些資料缺乏合理的物件歸屬,可使用 引入引數物件(Introduce Parameter Object) 為它們製造出一個“引數物件”。

收益

  • 更易讀,更簡短的程式碼。
  • 重構可能會暴露出之前未注意到的重複程式碼。

何時忽略

  • 這裡有一個重要的例外:有時候你明顯不想造成"被呼叫物件"與"較大物件"間的某種依賴關係。這時候將資料從物件中拆解出來單獨作為引數,也很合情理。但是請注意其所引發的代價。如果引數列太長或變化太頻繁,就需要重新考慮自己的依賴結構了。

重構方法說明

以函式取代引數(Replace Parameter with Methods)

問題

物件呼叫某個函式,並將所得結果作為引數,傳遞給另一個函式。而接受該引數的函式本身也能夠呼叫前一個函式。

int basePrice = quantity * itemPrice;
double seasonDiscount = this.getSeasonalDiscount();
double fees = this.getFees();
double finalPrice = discountedPrice(basePrice, seasonDiscount, fees);
複製程式碼

解決

讓引數接受者去除該項引數,並直接呼叫前一個函式。

int basePrice = quantity * itemPrice;
double finalPrice = discountedPrice(basePrice);
複製程式碼

保持物件完整(Preserve Whole Object)

問題

你從某個物件中取出若干值,將它們作為某一次函式呼叫時的引數。

int low = daysTempRange.getLow();
int high = daysTempRange.getHigh();
boolean withinPlan = plan.withinRange(low, high);
複製程式碼

解決

改為傳遞整個物件。

boolean withinPlan = plan.withinRange(daysTempRange);
複製程式碼

引入引數物件(Introduce Parameter Object)

問題

某些引數總是很自然地同時出現。


程式碼壞味道之程式碼臃腫

解決

以一個物件來取代這些引數。


程式碼壞味道之程式碼臃腫

擴充套件閱讀

參考資料

相關文章