程式碼壞味道之非必要的

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

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

翻譯自:https://sourcemaking.com/refactoring/smells/dispensables

非必要的(Dispensables)這組壞味道意味著:這樣的程式碼可有可無,它的存在反而影響整體程式碼的整潔和可讀性。

冗餘類

冗餘類(Lazy Class)

理解和維護總是費時費力的。如果一個類不值得你花費精力,它就應該被刪除。


程式碼壞味道之非必要的

問題原因

也許一個類的初始設計是一個功能完全的類,然而隨著程式碼的變遷,變得沒什麼用了。 又或者類起初的設計是為了支援未來的功能擴充套件,然而卻一直未派上用場。

解決方法

  • 沒什麼用的類可以運用 將類內聯化(Inline Class) 來幹掉。


程式碼壞味道之非必要的

  • 如果子類用處不大,試試 摺疊繼承體系(Collapse Hierarchy)

收益

  • 減少程式碼量
  • 易於維護

何時忽略

  • 有時,建立冗餘類是為了描述未來開發的意圖。在這種情況下,嘗試在程式碼中保持清晰和簡單之間的平衡。

重構方法說明

將類內聯化(Inline Class)

問題

某個類沒有做太多事情。


程式碼壞味道之非必要的

解決

將這個類的所有特性搬移到另一個類中,然後移除原類。


程式碼壞味道之非必要的

摺疊繼承體系(Collapse Hierarchy)

問題

超類和子類之間無太大區別。


程式碼壞味道之非必要的

解決

將它們合為一體。


程式碼壞味道之非必要的

誇誇其談未來性

誇誇其談未來性(Speculative Generality)

存在未被使用的類、函式、欄位或引數。


程式碼壞味道之非必要的

問題原因

有時,程式碼僅僅為了支援未來的特性而產生,然而卻一直未實現。結果,程式碼變得難以理解和維護。

解決方法

  • 如果你的某個抽象類其實沒有太大作用,請運用 摺疊繼承體系(Collapse Hierarch)


程式碼壞味道之非必要的

  • 不必要的委託可運用 將類內聯化(Inline Class) 消除。
  • 無用的函式可運用 行內函數(Inline Method) 消除。
  • 函式中有無用的引數應該運用 移除引數(Remove Parameter) 消除。
  • 無用欄位可以直接刪除。

收益

  • 減少程式碼量。
  • 更易維護。

何時忽略

  • 如果你在一個框架上工作,建立框架本身沒有使用的功能是非常合理的,只要框架的使用者需要這個功能。
  • 刪除元素之前,請確保它們不在單元測試中使用。如果測試需要從類中獲取某些內部資訊或執行特殊的測試相關操作,就會發生這種情況。

重構方法說明

摺疊繼承體系(Collapse Hierarchy)

問題

超類和子類之間無太大區別。


程式碼壞味道之非必要的

解決

將它們合為一體。


程式碼壞味道之非必要的

將類內聯化(Inline Class)

問題

某個類沒有做太多事情。


程式碼壞味道之非必要的

解決

將這個類的所有特性搬移到另一個類中,然後移除原類。


程式碼壞味道之非必要的

行內函數(Inline Method)

問題

一個函式的本體比函式名更清楚易懂。

class PizzaDelivery {
  //...
  int getRating() {
    return moreThanFiveLateDeliveries() ? 2 : 1;
  }
  boolean moreThanFiveLateDeliveries() {
    return numberOfLateDeliveries > 5;
  }
}
複製程式碼

解決

在函式呼叫點插入函式本體,然後移除該函式。

class PizzaDelivery {
  //...
  int getRating() {
    return numberOfLateDeliveries > 5 ? 2 : 1;
  }
}
複製程式碼

移除引數(Remove Parameter)

問題

函式本體不再需要某個引數。


程式碼壞味道之非必要的

解決

將該引數去除。


程式碼壞味道之非必要的

純稚的資料類

純稚的資料類(Data Class) 指的是隻包含欄位和訪問它們的 getter 和 setter 函式的類。這些僅僅是供其他類使用的資料容器。這些類不包含任何附加功能,並且不能對自己擁有的資料進行獨立操作。


程式碼壞味道之非必要的

問題原因

當一個新建立的類只包含幾個公共欄位(甚至可能幾個 getters / setters)是很正常的。但是物件的真正力量在於它們可以包含作用於資料的行為型別或操作。

解決方法

  • 如果一個類有公共欄位,你應該運用 封裝欄位(Encapsulated Field) 來隱藏欄位的直接訪問方式。
  • 如果這些類含容器類的欄位,你應該檢查它們是不是得到了恰當的封裝;如果沒有,就運用 封裝集合(Encapsulated Collection) 把它們封裝起來。
  • 找出這些 getter/setter 函式被其他類運用的地點。嘗試以 搬移函式(Move Method) 把那些呼叫行為搬移到 純稚的資料類(Data Class) 來。如果無法搬移這個函式,就運用 提煉函式(Extract Method) 產生一個可搬移的函式。


程式碼壞味道之非必要的

  • 在類已經充滿了深思熟慮的函式之後,你可能想要擺脫舊的資料訪問方法,以提供適應面較廣的類資料訪問介面。為此,可以運用 移除設定函式(Remove Setting Method)隱藏函式(Hide Method)

收益

  • 提高程式碼的可讀性和組織性。特定資料的操作現在被集中在一個地方,而不是在分散在程式碼各處。
  • 幫助你發現客戶端程式碼的重複處。

重構方法說明

封裝欄位(Encapsulated Field)

問題

你的類中存在 public 欄位。

class Person {
  public String name;
}
複製程式碼

解決

將它宣告為 private,並提供相應的訪問函式。

class Person {
  private String name;

  public String getName() {
    return name;
  }
  public void setName(String arg) {
    name = arg;
  }
}
複製程式碼

封裝集合(Encapsulated Collection)

問題

有個函式返回一個集合。


程式碼壞味道之非必要的

解決

讓該函式返回該集合的一個只讀副本,並在這個類中提供新增、移除集合元素的函式。


程式碼壞味道之非必要的

搬移函式(Move 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);
}
複製程式碼

移除設定函式(Remove Setting Method)

問題

類中的某個欄位應該在物件建立時被設值,然後就不再改變。


程式碼壞味道之非必要的

解決

去掉該欄位的所有設值函式。


程式碼壞味道之非必要的

隱藏函式(Hide Method)

問題

有一個函式,從來沒有被其他任何類用到。


程式碼壞味道之非必要的

解決

將這個函式修改為 private。


程式碼壞味道之非必要的

過多的註釋

過多的註釋(Comments)

註釋本身並不是壞事。但是常常有這樣的情況:一段程式碼中出現長長的註釋,而它之所以存在,是因為程式碼很糟糕。


程式碼壞味道之非必要的

問題原因

註釋的作者意識到自己的程式碼不直觀或不明顯,所以想使用註釋來說明自己的意圖。這種情況下,註釋就像是爛程式碼的除臭劑。

最好的註釋是為函式或類起一個恰當的名字。

如果你覺得一個程式碼片段沒有註釋就無法理解,請先嚐試重構,試著讓所有註釋都變得多餘。

解決方法

  • 如果一個註釋是為了解釋一個複雜的表示式,可以運用 提煉變數(Extract Variable) 將表示式切分為易理解的子表示式。
  • 如果你需要通過註釋來解釋一段程式碼做了什麼,請試試 提煉函式(Extract Method)
  • 如果函式已經被提煉,但仍需要註釋函式做了什麼,試試運用 函式改名(Rename Method) 來為函式起一個可以自解釋的名字。
  • 如果需要對系統某狀態進行斷言,請運用 引入斷言(Introduce Assertion)

收益

  • 程式碼變得更直觀和明顯。

何時忽略

註釋有時候很有用:

  • 當解釋為什麼某事物要以特殊方式實現時。
  • 當解釋某種複雜演算法時。
  • 當你實在不知可以做些什麼時。

重構方法說明

提煉變數(Extract Variable)

問題

你有個難以理解的表示式。

void renderBanner() {
  if ((platform.toUpperCase().indexOf("MAC") > -1) &&
       (browser.toUpperCase().indexOf("IE") > -1) &&
        wasInitialized() && resize > 0 )
  {
    // do something
  }
}
複製程式碼

解決

將表示式的結果或它的子表示式的結果用不言自明的變數來替代。

void renderBanner() {
  final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
  final boolean isIE = browser.toUpperCase().indexOf("IE") > -1;
  final boolean wasResized = resize > 0;

  if (isMacOs && isIE && wasInitialized() && wasResized) {
    // do something
  }
}
複製程式碼

提煉函式(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);
}
複製程式碼

函式改名(Rename Method)

問題

函式的名稱未能恰當的揭示函式的用途。

class Person {
  public String getsnm();
}
複製程式碼

解決

修改函式名。

class Person {
  public String getSecondName();
}
複製程式碼

引入斷言(Introduce Assertion)

問題

某一段程式碼需要對程式狀態做出某種假設。

double getExpenseLimit() {
  // should have either expense limit or a primary project
  return (expenseLimit != NULL_EXPENSE) ?
    expenseLimit:
    primaryProject.getMemberExpenseLimit();
}
複製程式碼

解決

以斷言明確表現這種假設。

double getExpenseLimit() {
  Assert.isTrue(expenseLimit != NULL_EXPENSE || primaryProject != null);

  return (expenseLimit != NULL_EXPENSE) ?
    expenseLimit:
    primaryProject.getMemberExpenseLimit();
}
複製程式碼

注:請不要濫用斷言。不要使用它來檢查”應該為真“的條件,只能使用它來檢查“一定必須為真”的條件。實際上,斷言更多是用於自我檢測程式碼的一種手段。在產品真正交付時,往往都會消除所有斷言。

重複程式碼

重複程式碼(Duplicate Code)

重複程式碼堪稱為程式碼壞味道之首。消除重複程式碼總是有利無害的。


程式碼壞味道之非必要的

問題原因

重複程式碼通常發生在多個程式設計師同時在同一程式的不同部分上工作時。由於他們正在處理不同的任務,他們可能不知道他們的同事已經寫了類似的程式碼。

還有一種更隱晦的重複,特定部分的程式碼看上去不同但實際在做同一件事。這種重複程式碼往往難以找到和消除。

有時重複是有目的性的。當急於滿足 deadline,並且現有程式碼對於要交付的任務是“幾乎正確的”時,新手程式設計師可能無法抵抗複製和貼上相關程式碼的誘惑。在某些情況下,程式設計師只是太懶惰。

解決方法

  • 同一個類的兩個函式含有相同的表示式,這時可以採用 提煉函式(Extract Method) 提煉出重複的程式碼,然後讓這兩個地點都呼叫被提煉出來的那段程式碼。


程式碼壞味道之非必要的

  • 如果兩個互為兄弟的子類含有重複程式碼:
    • 首先對兩個類都運用 提煉函式(Extract Method) ,然後對被提煉出來的函式運用 函式上移(Pull Up Method) ,將它推入超類。
    • 如果重複程式碼在建構函式中,運用 建構函式本體上移(Pull Up Constructor Body)
    • 如果重複程式碼只是相似但不是完全相同,運用 塑造模板函式(Form Template Method) 獲得一個 模板方法模式(Template Method)
    • 如果有些函式以不同的演算法做相同的事,你可以選擇其中較清晰地一個,並運用 替換演算法(Substitute Algorithm) 將其他函式的演算法替換掉。
  • 如果兩個毫不相關的類中有重複程式碼:
    • 請嘗試運用 提煉超類(Extract Superclass) ,以便為維護所有先前功能的這些類建立一個超類。
    • 如果建立超類十分困難,可以在一個類中運用 提煉類(Extract Class) ,並在另一個類中使用這個新的元件。
  • 如果存在大量的條件表示式,並且它們執行完全相同的程式碼(僅僅是它們的條件不同),可以運用 合併條件表示式(Consolidate Conditional Expression) 將這些操作合併為單個條件,並運用 提煉函式(Extract Method) 將該條件放入一個名字容易理解的獨立函式中。
  • 如果條件表示式的所有分支都有部分相同的程式碼片段:可以運用 合併重複的條件片段(Consolidate Duplicate Conditional Fragments) 將它們都存在的程式碼片段置於條件表示式外部。

收益

  • 合併重複程式碼會簡化程式碼的結構,並減少程式碼量。
  • 程式碼更簡化、更易維護。

重構方法說明

提煉函式(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);
}
複製程式碼

函式上移(Pull Up Method)

問題

有些函式,在各個子類中產生完全相同的結果。


程式碼壞味道之非必要的

解決

將該函式移至超類。


程式碼壞味道之非必要的

建構函式本體上移(Pull Up Constructor Body)

問題

你在各個子類中擁有一些建構函式,它們的本體幾乎完全一致。

class Manager extends Employee {
  public Manager(String name, String id, int grade) {
    this.name = name;
    this.id = id;
    this.grade = grade;
  }
  //...
}
複製程式碼

解決

在超類中新建一個建構函式,並在子類建構函式中呼叫它。

class Manager extends Employee {
  public Manager(String name, String id, int grade) {
    super(name, id);
    this.grade = grade;
  }
  //...
}
複製程式碼

塑造模板函式(Form Template Method)

問題

你有一些子類,其中相應的某些函式以相同的順序執行類似的操作,但各個操作的細節上有所不同。


程式碼壞味道之非必要的

解決

將這些操作分別放進獨立函式中,並保持它們都有相同的簽名,於是原函式也就變得相同了。然後將原函式上移至超類。


程式碼壞味道之非必要的

注:這裡只提到具體做法,建議瞭解一下模板方法設計模式。

替換演算法(Substitute Algorithm)

問題

你想要把某個演算法替換為另一個更清晰的演算法。

String foundPerson(String[] people){
  for (int i = 0; i < people.length; i++) {
    if (people[i].equals("Don")){
      return "Don";
    }
    if (people[i].equals("John")){
      return "John";
    }
    if (people[i].equals("Kent")){
      return "Kent";
    }
  }
  return "";
}
複製程式碼

解決

將函式本體替換為另一個演算法。

String foundPerson(String[] people){
  List candidates =
    Arrays.asList(new String[] {"Don", "John", "Kent"});
  for (int i=0; i < people.length; i++) {
    if (candidates.contains(people[i])) {
      return people[i];
    }
  }
  return "";
}
複製程式碼

提煉超類(Extract Superclass)

問題

兩個類有相似特性。


程式碼壞味道之非必要的

解決

為這兩個類建立一個超類,將相同特性移至超類。


程式碼壞味道之非必要的

提煉類(Extract Class)

問題

某個類做了不止一件事。


程式碼壞味道之非必要的

解決

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


程式碼壞味道之非必要的

合併條件表示式(Consolidate Conditional Expression)

問題

你有一系列條件分支,都得到相同結果。

double disabilityAmount() {
  if (seniority < 2) {
    return 0;
  }
  if (monthsDisabled > 12) {
    return 0;
  }
  if (isPartTime) {
    return 0;
  }
  // compute the disability amount
  //...
}
複製程式碼

解決

將這些條件分支合併為一個條件,並將這個條件提煉為一個獨立函式。

double disabilityAmount() {
  if (isNotEligableForDisability()) {
    return 0;
  }
  // compute the disability amount
  //...
}
複製程式碼

合併重複的條件片段(Consolidate Duplicate Conditional Fragments)

問題

在條件表示式的每個分支上有著相同的一段程式碼。

if (isSpecialDeal()) {
  total = price * 0.95;
  send();
}
else {
  total = price * 0.98;
  send();
}
複製程式碼

解決

將這段重複程式碼搬移到條件表示式之外。

if (isSpecialDeal()) {
  total = price * 0.95;
}
else {
  total = price * 0.98;
}
send();
複製程式碼

擴充套件閱讀

參考資料

相關文章