以建構方法取代多個建構函式

Replace Multiple Constructors with Creation Methods*

 

Joshua Kerievsky      透明

 

Copyright (C) 2001, Joshua Kerievsky, Industrial Logic, Inc.

UMLCHINA 保留本文中譯本一切權利

 

    如果一個類別中有多個建構函式,在開發過程中將難以決定究竟選擇哪一個。

    用能表現意圖的建構方法(Creation Method)返回物件實例,從而取代建構函式的作用。

 

動機

      某些語言允許你用自己喜歡的任何方式為自己的建構函式命名,而不用管類別的名字。另一些語言(例如 C++ Java)則不允許這樣做:每個 建構函式都必須按照所屬的類別的名字來命名。如果只有一個建構函式,不成問題;但是如果擁有多個建構函式,程式師就必須去瞭解建構函式期望的參數、觀察建構函式的程式碼,這樣才能正確選擇自己要使用的 建構函式。這有什麼毛病?毛病太多了。多個建構函式根本無法有效描述意圖。擁有的建構函式越多,程式師就越容易選擇錯誤。而且,程式師不得不去選擇使用哪個建構函式,這降低了開發的速度,而且呼叫建構函式的程式碼經常不能有效的與建構出來的物件交流。

 

      如果你覺得這聽起來很糟糕,還有更糟糕的呢。隨著系統日趨成熟,程式師們經常會加入越來越多的建構函式,而不去檢查以前的建構函式是否仍在使用。不再使用的建構函式仍然留在類 別中,這增加了類別的靜態負載,只會讓類別變得愈加臃腫而複雜。成熟的軟體系統中往往充斥著這種“死建構函式”,因為程式師沒有快速而簡單的途徑來識別某個建構函式是否仍被 呼叫:IDE幫不了他們,判斷某個方法確切的 呼叫者所需的搜索語句又實在太麻煩。另一方面,如果物件建構呼叫主要透過一個特定名稱的方法來進行,例如 createTermLoad() 或者 createRevolver(),找到這些方法所有的 呼叫者是輕而易舉的。

 

      那麼,我們的同行們通常把建構物件的方法叫做什麼?許多人都會回答“工廠方法(Factory Method)”,這是因為那本經典著作《設計 樣式》[GoF]這樣稱呼一個建構型樣式。但是,所有建構物件的方法真的都是工廠方法嗎?如果給“工廠方法”這個術語一個更寬的定義:只要建構物件的方法都叫工廠方法,那麼答案肯定是“Yes”。但是按照這個建構型樣式(Factory Method 樣式)的作者撰寫它的方式來看,很明顯並非所有建構物件的方法都能提供真正的工廠方法所提供的那種鬆耦合。因此,為了在討論與物件建構相關的設計和重整時保證大家的清醒,我用“建構方法(Creation Method)”這個術語來表示“一個建構物件的方法”。也就是說:工廠方法都是建構方法,但相反則未必。這還意味著:在任何 Martin Fowler Joshua Bloch 使用“工廠方法”這個術語(分別在他們精彩的書《Refactoring[Fowler]和《Effective Java[Bloch]中)時,你都可以代之以“建構方法”。

 

溝通

重複

簡化

多個建構函式不是一種好的溝通形式——很明顯,通過能揭示意圖的建構方法提供對實例的訪問,這是一種好的溝通形式。

沒有直接的重複,只是有許多看上去幾乎完全一樣的建構函式。

指出應該呼叫哪個建構函式並不是一件容易的事。通過能揭示意圖的建構方法來建構不同類型的物件,這個問題就簡單多了。

 

技巧

  1. 識別出擁有多個建構函式的類別。這些建構函式通常有一大堆參數,而這就讓開發者需要獲得一個實例的時候更加迷惑了。

  2. 識別出 catch-all 建構函式,或者用 Chain Constructor(見第 8 期《非程式師》)建構一個 catch-all 建構函式。如果 catch-all 建構函式的可見性是 public,將它改為 private protected

  3. 針對每個建構函式所能建構的那種物件,建構一個能夠揭示意圖的建構方法。測試,確保每個建構方法都傳回正確的物件,確定每個建構方法都被客戶程式碼呼叫到了(如果某個 建構方法沒有使用者,將它刪掉;到需要它的時候再放回來)。

  4. 把所有對建構函式的呼叫都換成對相應建構方法的呼叫。這需要費些力氣,但是可以使客戶程式碼的易讀性大大提高。

 

範例

        1、在下面的示例程式碼段中,我們擁有一個 Loan 類 別,其中有多個建構函式,分別表示定期貸款(Term Loan)、活期貸款(Revolver)和定活兩便貸款(RCTL)。[1]

 

public class Loan {

private static String TERM_LOAN = “TL”;

private static String REVOLVER = “RC”;

private static String RCTL = “RCTL”;

private String type;

private CapitalStrategy strategy;

private float notional;

private float outstanding;

private int customerRating;

private Date maturity;

private Date expiry;

 

public Loan(float notional, float outstanding, int customerRating, Date expiry) {

this(TERM_LOAN, new TermROC(), notional, outstanding,

customerRating, expiry, null);

}

public Loan(float notional, float outstanding, int customerRating, Date expiry,

Date maturity) {

this(RCTL, new RevolvingTermROC(), notional, outstanding, customerRating,

expiry, maturity);

}

public Loan(CapitalStrategy strategy, float notional, float outstanding,

int customerRating, Date expiry, Date maturity) {

this(RCTL, strategy, notional, outstanding, customerRating,

expiry, maturity);

}

public Loan(String type, CapitalStrategy strategy, float notional,

float outstanding, int customerRating, Date expiry) {

this(type, strategy, notional, outstanding, customerRating, expiry, null);

}

public Loan(String type, CapitalStrategy strategy, float notional,

float outstanding, int customerRating, Date expiry, Date maturity) {

this.type = type;

this.strategy = strategy;

this.notional = notional;

this.outstanding = outstanding;

this.customerRating = customerRating;

this.expiry = expiry;

if (RCTL.equals(type))

this.maturity = maturity;

}

}

 

        這個類別中有 5 個 建構函式,最後的一個是 catch-all 建構函式。如果只是用看的,你很難知道究竟哪一個 建構函式建構哪種物件。我碰巧知道 RCTL 既需要終止日期也需要到期日期,所以我知道要建構 RCTL 物件必須 呼叫讓我傳進這兩個日期的建構函式。但是使用這個類別的其他程式師是否也知道這一點呢?如果他們不知道,建構函式能與他們充分溝通嗎?當然了,費點力氣,他們也能找出這些規律。但是他們本不應該費這些力氣來獲取所需的 Loan 物件的。

 

      在繼續進行重整之前,我需要知道上面這些建構函式還做出了什麼其他的假設。這埵酗@個比較重要的:如果呼叫第一個建構函式,你會得到一個定期貸款物件而不是活期貸款物件。如果你需要的是活期貸款物件,請 呼叫最後兩個建構函式,它們會要求你傳遞貸款類型參數進去。唔…… 這個類別所有的用戶都知道這一點嗎?我很懷疑。或者他們是否一定會遇到一些非常討厭的 bug,然後才能學到這些?

 

        2、下一步的任務是要識別出 Loan 類 別的 catch-all 建構函式。很簡單——接收參數最多的那一個就是:

 

public Loan(String type, CapitalStrategy strategy, float notional, float outstanding,

int customerRating, Date expiry, Date maturity) {

this.type = type;

this.strategy = strategy;

this notional = notional;

this.outstanding = outstanding;

this.customerRating = customerRating;

this.expiry = expiry;

if (RCTL.equals(type)

this.maturity = maturity;

}

 

        把這個建構函式聲明為 protected

 

protected Loan(String type, CapitalStrategy strategy, float notional, float outstanding,

int customerRating, Date expiry, Date maturity)

 

        3、然後,我們必須判斷出 Loan 類別的每個建構函式建構的物件種類。在這個例子中,有下列的種類:

       首先我要為新的建構方法編寫測試,讓它用預設的定期貸款資本策略返回一個定期貸款物件。

 

public void testTermLoan() {

String custRating = 2;

Date expiry = createDate(2001, Calendar.NOVEMBER, 20);

Loan loan = Loan.newTermLoan(1000f, 250f, CUSTOMER_RATING, expiry);

assertNotNull(loan);

assertEquals(Loan.TERM_LOAN, loan.getType());

}

 

       這個測試無法編譯運行,直到我為 Loan 類別加入下面這個靜態方法:

 

public class Loan...

static Loan newTermLoan(float notional, float outstanding, int customerRating,

Date expiry) {

return new Loan(TERM_LOAN, new TermROC(), notional, outstanding,

customerRating, expiry, null);

}

 

        請注意看這個方法如何代理步驟 1 中識別出的 protected catch-all 建構函式。我還建構 5個類似的測試和 5 個附加的能夠揭示意圖的建構方法,對應於剩下的 5 種物件。在這項工作完成之後,Loan 類別已經沒有 public 的建構函式了。重整完成後的 Loan 類別看起來像這樣:

 

public class Loan {

private static String TERM_LOAN = “TL”;

private static String REVOLVER = “RC”;

private static String RCTL = “RCTL”;

private String type;

private CapitalStrategy strategy;

private float notional;

private float outstanding;

private int customerRating;

private Date maturity;

private Date expiry;

 

protected Loan(String type, CapitalStrategy strategy, float notional,

float outstanding, int customerRating, Date expiry, Date maturity) {

this.type = type;

this.strategy = strategy;

this notional = notional;

this.outstanding = outstanding;

this.customerRating = customerRating;

this.expiry = expiry;

if (RCTL.equals(type)

this.maturity = maturity;

}

 

static Loan newTermLoan(float notional, float outstanding, int customerRating,

Date expiry) {

return new Loan(TERM_LOAN, new TermROC(), notional, outstanding, customerRating,

expiry, null);

}

static Loan newTermWithStrategy(CapitalStrategy strategy, float notional,

float outstanding, int customerRating, Date expiry) {

return new Loan(TERM_LOAN, strategy, new TermROC(), notional, outstanding,

customerRating, expiry, null);

}

static Loan newRevolver(float notional, float outstanding, int customerRating,

Date expiry) {

return new Loan(REVOLVER, new RevolverROC(), notional, outstanding,

customerRating, expiry, null);

}

static Loan newRevolverWithStrategy(CapitalStrategy strategy, float notional,

float outstanding, int customerRating, Date expiry) {

return new Loan(REVOLVER, strategy, new RevolverROC(), notional, outstanding,

customerRating, expiry, null);

}

static Loan newRCTL(float notional, float outstanding, int customerRating,

Date expiry, Date maturity) {

return new Loan(RCTL, new RCTLROC(), notional, outstanding,

customerRating, expiry, maturity);

}

static Loan newRCTLWithStrategy(CapitalStrategy strategy, float notional,

float outstanding, int customerRating, Date expiry, Date maturity) {

return new Loan(RCTL, strategy, new RevolverROC(), notional, outstanding,

customerRating, expiry, maturity);

}

}

 

        現在,如何獲取需要的 Loan 實例就很清楚了——你只需要看看自己需要哪種物件,然後呼叫合適的方法就行了。新的建構方法仍然有相當多的參數。Introduce Parameter Object[Fowler] 是一個可以幫助你減少傳遞給方法的參數的重整。

 

參數化建構方法

        在考慮實現這個重整的時候,你可能會在腦子媞滮@筆帳:為了支援你的類別提供的各種物件配置,需要大約 50 個建構方法。編寫 50 個方法,這聽起來可不是件有趣的事,所以你可能會決定不做這個重整。但是,也有辦法對付這種情形。首先,你不需要為每種物件配置都生成一個建構方法:你可以為幾種最常見的配置編寫建構方法,並留下一些 public 的建構函式來處理剩下的情況。另外,經常可以用參數來減少建構方法的數量——我們把它們叫做參數化建構方法。比如說,一個簡單的 Apple 類 別可以根據以下條件實例化:

       這些選項表現出了幾種不同類型的蘋果,儘管它們沒有被顯式定義為 Apple 類別的子類別。為了獲取你所需要的 Apple 實例,你必須呼叫恰當的建構函式。但是對應於很多類型的蘋果,Apple 類 別可能有很多建構函式:

 

public Apple(AppleFamily family, Color color) {

this(family, color, Country.USA, true, false);

}

public Apple(AppleFamily family, Color color, Country country) {

this(family, color, country, true, false);

}

public Apple(AppleFamily family, Color color, boolean hasSeeds) {

this(family, color, Country.USA, hasSeeds, false);

}

public Apple(AppleFamily family, Color color, Country country, boolean hasSeeds) {

this(family, color, country, hasSeeds, false);

}

public Apple(AppleFamily family, Color color, Country country,

boolean hasSeeds, boolean isPeeled) {

this.family = family;

this.color = color;

this.country = country;

this.hasSeeds = hasSeeds;

this.isPeeled = isPeeled;

}

        正如我們前面提到過的,這麼多的建構函式只會讓 Apple 類別的使用更加困難。為了改善 Apple 類別的可用性,而又不編寫大量的建構方法,我們可以識別出最常見的蘋果種類,並為它們準備建構方法:

public static Apple createSeedlessAmericanMacintosh();

public static Apple createSeedlessGrannySmith();

public static Apple createSeededAsianGoldenDelicious();

        這些建構方法不能完全替代 public 的建構函式,但是可以起到補充的作用,並且有可能減少建構函式的數量。但是,因為上面的建構方法不是參數化的,隨著時間的流逝,它們的數量很可能倍增,變成許多許多的建構方法,這也同樣會讓 Apple 類別的使用者難以選擇。因此,在面臨如此多的可能性時,編寫參數化建構方法通常都是有意義的:

public static Apple createSeedlessMacintosh(Country c);

public static Apple createGoldenDelicious(Country c);

 

參考文獻


 

[1] 譯注:這堛 Term Loan, Revolver, RCTL 這三個詞我都不太理解,請大家將就著吧。