代碼壞味道系列之濫用OO

濫用OO

濫用面向對象,代碼部分或徹底地違背了面向對象編程原則。java

oo-abusers

switch驚悚現身

代碼中有一個複雜的 switch 語句或 if 序列語句。git

switch-statements-01

問題緣由

面向對象程序的一個最明顯特徵就是:少用 switchcase 語句。從本質上說,switch 語句的問題在於重複(if 序列也一樣如此)。你常會發現 switch 語句散佈於不一樣地點。若是要爲它添加一個新的 case 子句,就必須找到全部 switch 語句並修改它們。面向對象中的多態概念可爲此帶來優雅的解決辦法。程序員

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

解決方案

  • 問題是多態該出如今哪?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-statements-02

什麼時候忽略

  • 若是一個 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)

問題this

你的程序中,有個函數與其所駐類以外的另外一個類進行更多交流:調用後者,或被後者調用。spa

move-method-before

解決

在該函數最常引用的類中創建一個有着相似行爲的新函數。將舊函數變成一個單純的委託函數,或是舊函數徹底移除。

move-method-after

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

問題

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

replace-type-code-with-subclasses-before

解決

以子類取代這個類型碼。

replace-type-code-with-subclasses-after

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

問題

你有一個類型碼,它會影響類的行爲,但你沒法經過繼承消除它。

replace-type-code-with-state-strategy-before

解決

以狀態對象取代類型碼。

replace-type-code-with-state-strategy-after

以多態取代條件表達式(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)的值只在特定環境下有意義,離開這個環境,它們就什麼也不是了。

temporary-field-01

問題緣由

有時你會看到這樣的對象:其內某個實例變量僅爲某種特定狀況而設。這樣的代碼讓人不易理解,由於你一般認爲對象在全部時候都須要它的全部變量。在變量未被使用的狀況下猜想當初設置目的,會讓你發瘋。
一般,臨時字段是在某一算法須要大量輸入時而建立。所以,爲了不函數有過多參數,程序員決定在類中建立這些數據的臨時字段。這些臨時字段僅僅在算法中使用,其餘時候卻毫無用處。
這種代碼很差理解。你指望查看對象字段的數據,可是出於某種緣由,它們老是爲空。

解決方案

  • 能夠經過 提煉類(Extract Class) 將臨時字段和操做它們的全部代碼提煉到一個單獨的類中。此外,你能夠運用 以函數對象取代函數(Replace Method with Method Object) 來實現一樣的目的。
  • 引入 Null 對象(Introduce Null Object) 在「變量不合法」的狀況下建立一個 null 對象,從而避免寫出條件表達式。

temporary-field-02

收益

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

temporary-field-03

重構方法說明

提煉類(Extract Class)

問題

某個類作了不止一件事。

extract-class-before

解決

創建一個新類,將相關的字段和函數從舊類搬移到新類。

extract-class-after

以函數對象取代函數(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();

被拒絕的遺贈

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

refused-bequest-01

問題緣由

有些人僅僅是想重用超類中的部分代碼而建立了子類。但實際上超類和子類徹底不一樣。

解決方案

  • 若是繼承沒有意義而且子類和父類之間確實沒有共同點,能夠運用 以委託取代繼承(Replace Inheritance with Delegation) 消除繼承。
  • 若是繼承是適當的,則去除子類中不須要的字段和方法。運用 提煉超類(Extract Superclass) 將全部超類中對於子類有用的字段和函數提取出來,置入一個新的超類中,而後讓兩個類都繼承自它。

refused-bequest-02

收益

  • 提升代碼的清晰度和組織性。

refused-bequest-03

重構方法說明

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

問題

某個子類只使用超類接口中的一部分,或是根本不須要繼承而來的數據。

replace-inheritance-with-delegation-before

解決

  1. 在子類中新建一個字段用以保存超類;
  2. 調整子類函數,令它改而委託超類;
  3. 而後去掉二者之間的繼承關係。

replace-inheritance-with-delegation-after

提煉超類(Extract Superclass)

問題

兩個類有類似特性。

extract-superclass-before

解決

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

extract-superclass-after

殊途同歸的類

兩個類中有着不一樣的函數,卻在作着同一件事。

alternative-classes-with-different-interfaces-01

問題緣由

這種狀況每每是由於:建立這個類的程序員並不知道已經有實現這個功能的類存在了。

解決方案

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

收益

  • 消除了沒必要要的重複代碼,爲代碼瘦身了。
  • 代碼更易讀(再也不須要猜想爲何要有兩個功能相同的類)。

alternative-classes-with-different-interfaces-02

什麼時候忽略

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

重構方法說明

函數更名(Rename Method)

問題

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

class Person {
  public String getsnm();
}

解決

修改函數名。

class Person {
  public String getSecondName();
}
搬移函數(Move Method)

問題

你的程序中,有個函數與其所駐類以外的另外一個類進行更多交流:調用後者,或被後者調用。

move-method-before

解決

在該函數最常引用的類中創建一個有着相似行爲的新函數。將舊函數變成一個單純的委託函數,或是舊函數徹底移除。

move-method-after

添加參數(Add Parameter)

問題
某個函數須要從調用端獲得更多信息。

class Customer {
  public Contact getContact();
}

解決
爲此函數添加一個對象函數,讓改對象帶進函數所需信息。

class Customer {
  public Contact getContact(Date date);
}

令函數攜帶參數(Parameterize Method)

問題

若干函數作了相似的工做,但在函數本體中卻包含了不一樣的值。

parameterize-method-before

解決

創建單一函數,以參數表達哪些不一樣的值。

parameterize-method-after

提煉超類(Extract Superclass)

問題

兩個類有類似特性。

extract-superclass-before

解決

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

extract-superclass-after

擴展閱讀

參考資料

相關文章
相關標籤/搜索