:notebook: 本文已歸檔到:「blog」
翻譯自:https://sourcemaking.com/refactoring/smells/oo-abusers
濫用物件導向(Object-Orientation Abusers)這組壞味道意味著:程式碼部分或完全地違背了物件導向程式設計原則。
Switch 宣告
Switch 宣告(Switch Statements)
你有一個複雜的
switch
語句或if
序列語句。
問題原因
物件導向程式的一個最明顯特徵就是:少用 switch
和 case
語句。從本質上說,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)
問題
某個子類只使用超類介面中的一部分,或是根本不需要繼承而來的資料。
解決
- 在子類中新建一個欄位用以儲存超類;
- 調整子類函式,令它改而委託超類;
- 然後去掉兩者之間的繼承關係。
提煉超類(Extract Superclass)
問題
兩個類有相似特性。
解決
為這兩個類建立一個超類,將相同特性移至超類。
擴充套件閱讀
參考資料
- 重構——改善既有程式碼的設計 - by Martin Fowler
- https://sourcemaking.com/refactoring