程式碼壞味道之濫用物件導向

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

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

翻譯自:https://sourcemaking.com/refactoring/smells/oo-abusers

濫用物件導向(Object-Orientation Abusers)這組壞味道意味著:程式碼部分或完全地違背了物件導向程式設計原則。

Switch 宣告

Switch 宣告(Switch Statements)

你有一個複雜的 switch 語句或 if 序列語句。


程式碼壞味道之濫用物件導向

問題原因

物件導向程式的一個最明顯特徵就是:少用 switchcase 語句。從本質上說,switch 語句的問題在於重複(if 序列也同樣如此)。你常會發現 switch 語句散佈於不同地點。如果要為它新增一個新的 case 子句,就必須找到所有 switch 語句並修改它們。物件導向中的多型概念可為此帶來優雅的解決辦法。

大多數時候,一看到 switch 語句,就應該考慮以多型來替換它。

解決方法

  • 問題是多型該出現在哪?switch 語句常常根據型別碼進行選擇,你要的是“與該型別碼相關的函式或類”,所以應該運用 提煉函式(Extract Method)switch 語句提煉到一個獨立函式中,再以 搬移函式(Move Method) 將它搬移到需要多型性的那個類裡。
  • 如果你的 switch 是基於型別碼來識別分支,這時可以運用 以子類取代型別碼(Replace Type Code with Subclass)以狀態/策略模式取代型別碼(Replace Type Code with State/Strategy)
  • 一旦完成這樣的繼承結構後,就可以運用 以多型取代條件表示式(Replace Conditional with Polymorphism) 了。
  • 如果條件分支並不多並且它們使用不同引數呼叫相同的函式,多型就沒必要了。在這種情況下,你可以運用 以明確函式取代引數(Replace Parameter with Explicit Methods)
  • 如果你的選擇條件之一是 null,可以運用 引入 Null 物件(Introduce Null Object)

收益

  • 提升程式碼組織性。


程式碼壞味道之濫用物件導向

何時忽略

  • 如果一個 switch 操作只是執行簡單的行為,就沒有重構的必要了。
  • switch 常被工廠設計模式族(工廠方法模式(Factory Method)抽象工廠模式(Abstract Factory))所使用,這種情況下也沒必要重構。

重構方法說明

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

搬移函式(Move Method)

問題

你的程式中,有個函式與其所駐類之外的另一個類進行更多交流:呼叫後者,或被後者呼叫。


程式碼壞味道之濫用物件導向

解決

在該函式最常引用的類中建立一個有著類似行為的新函式。將舊函式變成一個單純的委託函式,或是舊函式完全移除。


程式碼壞味道之濫用物件導向

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

問題

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


程式碼壞味道之濫用物件導向

解決

以子類取代這個型別碼。


程式碼壞味道之濫用物件導向

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

問題

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


程式碼壞味道之濫用物件導向

解決

以狀態物件取代型別碼。


程式碼壞味道之濫用物件導向

以多型取代條件表示式(Replace Conditional with Polymorphism)

問題

你手上有個條件表示式,它根據物件型別的不同而選擇不同的行為。

class Bird {
  //...
  double getSpeed() {
    switch (type) {
      case EUROPEAN:
        return getBaseSpeed();
      case AFRICAN:
        return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
      case NORWEGIAN_BLUE:
        return (isNailed) ? 0 : getBaseSpeed(voltage);
    }
    throw new RuntimeException("Should be unreachable");
  }
}
複製程式碼

解決

將這個條件表示式的每個分支放進一個子類內的覆寫函式中,然後將原始函式宣告為抽象函式。

abstract class Bird {
  //...
  abstract double getSpeed();
}

class European extends Bird {
  double getSpeed() {
    return getBaseSpeed();
  }
}
class African extends Bird {
  double getSpeed() {
    return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
  }
}
class NorwegianBlue extends Bird {
  double getSpeed() {
    return (isNailed) ? 0 : getBaseSpeed(voltage);
  }
}

// Somewhere in client code
speed = bird.getSpeed();
複製程式碼

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

問題

你有一個函式,其中完全取決於引數值而採取不同的行為。

void setValue(String name, int value) {
  if (name.equals("height")) {
    height = value;
    return;
  }
  if (name.equals("width")) {
    width = value;
    return;
  }
  Assert.shouldNeverReachHere();
}
複製程式碼

解決

針對該引數的每一個可能值,建立一個獨立函式。

void setHeight(int arg) {
  height = arg;
}
void setWidth(int arg) {
  width = arg;
}
複製程式碼

引入 Null 物件(Introduce Null Object)

問題

你需要再三檢查某物件是否為 null。

if (customer == null) {
  plan = BillingPlan.basic();
}
else {
  plan = customer.getPlan();
}
複製程式碼

解決

將 null 值替換為 null 物件。

class NullCustomer extends Customer {
  Plan getPlan() {
    return new NullPlan();
  }
  // Some other NULL functionality.
}

// Replace null values with Null-object.
customer = (order.customer != null) ? order.customer : new NullCustomer();

// Use Null-object as if it's normal subclass.
plan = customer.getPlan();
複製程式碼

臨時欄位

臨時欄位(Temporary Field)的值只在特定環境下有意義,離開這個環境,它們就什麼也不是了。


程式碼壞味道之濫用物件導向

問題原因

有時你會看到這樣的物件:其內某個例項變數僅為某種特定情況而設。這樣的程式碼讓人不易理解,因為你通常認為物件在所有時候都需要它的所有變數。在變數未被使用的情況下猜測當初設定目的,會讓你發瘋。 通常,臨時欄位是在某一演算法需要大量輸入時而建立。因此,為了避免函式有過多引數,程式設計師決定在類中建立這些資料的臨時欄位。這些臨時欄位僅僅在演算法中使用,其他時候卻毫無用處。 這種程式碼不好理解。你期望檢視物件欄位的資料,但是出於某種原因,它們總是為空。

解決方法

  • 可以通過 提煉類(Extract Class) 將臨時欄位和操作它們的所有程式碼提煉到一個單獨的類中。此外,你可以運用 以函式物件取代函式(Replace Method with Method Object) 來實現同樣的目的。
  • 引入 Null 物件(Introduce Null Object) 在“變數不合法”的情況下建立一個 null 物件,從而避免寫出條件表示式。


程式碼壞味道之濫用物件導向

收益

  • 更好的程式碼清晰度和組織性。


程式碼壞味道之濫用物件導向

重構方法說明

提煉類(Extract Class)

問題

某個類做了不止一件事。


程式碼壞味道之濫用物件導向

解決

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


程式碼壞味道之濫用物件導向

以函式物件取代函式(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.
    //...
  }
}
複製程式碼

引入 Null 物件(Introduce Null Object)

問題

你需要再三檢查某物件是否為 null。

if (customer == null) {
  plan = BillingPlan.basic();
}
else {
  plan = customer.getPlan();
}
複製程式碼

解決

將 null 值替換為 null 物件。

class NullCustomer extends Customer {
  Plan getPlan() {
    return new NullPlan();
  }
  // Some other NULL functionality.
}

// Replace null values with Null-object.
customer = (order.customer != null) ? order.customer : new NullCustomer();

// Use Null-object as if it's normal subclass.
plan = customer.getPlan();
複製程式碼

異曲同工的類

異曲同工的類(Alternative Classes with Different Interfaces)

兩個類中有著不同的函式,卻在做著同一件事。


程式碼壞味道之濫用物件導向

問題原因

這種情況往往是因為:建立這個類的程式設計師並不知道已經有實現這個功能的類存在了。

解決方法

  • 如果兩個函式做同一件事,卻有著不同的簽名,請運用 函式改名(Rename Method) 根據它們的用途重新命名。
  • 運用 搬移函式(Move Method)新增引數(Add Parameter)令函式攜帶引數(Parameterize Method) 來使得方法的名稱和實現一致。
  • 如果兩個類僅有部分功能是重複的,嘗試運用 提煉超類(Extract Superclass) 。這種情況下,已存在的類就成了超類。
  • 當最終選擇並運用某種方法來重構後,也許你就能刪除其中一個類了。

收益

  • 消除了不必要的重複程式碼,為程式碼瘦身了。
  • 程式碼更易讀(不再需要猜測為什麼要有兩個功能相同的類)。


程式碼壞味道之濫用物件導向

何時忽略

  • 有時合併類是不可能的,或者是如此困難以至於沒有意義。例如:兩個功能相似的類存在於不同的 lib 庫中。

重構方法說明

函式改名(Rename Method)

問題

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

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

解決

修改函式名。

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

搬移函式(Move Method)

問題

你的程式中,有個函式與其所駐類之外的另一個類進行更多交流:呼叫後者,或被後者呼叫。


程式碼壞味道之濫用物件導向

解決

在該函式最常引用的類中建立一個有著類似行為的新函式。將舊函式變成一個單純的委託函式,或是舊函式完全移除。


程式碼壞味道之濫用物件導向

新增引數(Add Parameter)

問題 某個函式需要從呼叫端得到更多資訊。

class Customer {
  public Contact getContact();
}
複製程式碼

解決 為此函式新增一個物件函式,讓改物件帶進函式所需資訊。

class Customer {
  public Contact getContact(Date date);
}
複製程式碼

令函式攜帶引數(Parameterize Method)

問題

若干函式做了類似的工作,但在函式本體中卻包含了不同的值。


程式碼壞味道之濫用物件導向

解決

建立單一函式,以參數列達哪些不同的值。


程式碼壞味道之濫用物件導向

提煉超類(Extract Superclass)

問題

兩個類有相似特性。


程式碼壞味道之濫用物件導向

解決

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


程式碼壞味道之濫用物件導向

被拒絕的饋贈

被拒絕的饋贈(Refused Bequest)

子類僅僅使用父類中的部分方法和屬性。其他來自父類的饋贈成為了累贅。


程式碼壞味道之濫用物件導向

問題原因

有些人僅僅是想重用超類中的部分程式碼而建立了子類。但實際上超類和子類完全不同。

解決方法

  • 如果繼承沒有意義並且子類和父類之間確實沒有共同點,可以運用 以委託取代繼承(Replace Inheritance with Delegation) 消除繼承。
  • 如果繼承是適當的,則去除子類中不需要的欄位和方法。運用 提煉超類(Extract Superclass) 將所有超類中對於子類有用的欄位和函式提取出來,置入一個新的超類中,然後讓兩個類都繼承自它。


程式碼壞味道之濫用物件導向

收益

  • 提高程式碼的清晰度和組織性。


程式碼壞味道之濫用物件導向

重構方法說明

以委託取代繼承(Replace Inheritance with Delegation)

問題

某個子類只使用超類介面中的一部分,或是根本不需要繼承而來的資料。


程式碼壞味道之濫用物件導向

解決

  1. 在子類中新建一個欄位用以儲存超類;
  2. 調整子類函式,令它改而委託超類;
  3. 然後去掉兩者之間的繼承關係。


程式碼壞味道之濫用物件導向

提煉超類(Extract Superclass)

問題

兩個類有相似特性。


程式碼壞味道之濫用物件導向

解決

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


程式碼壞味道之濫用物件導向

擴充套件閱讀

參考資料

相關文章