呼叫父類

2005 年 8 月 11 日

呼叫父類是一種輕微的異味(或反模式),偶爾會在 OO 架構中出現。它的症狀很容易發現。您繼承自父類別,以便插入某些架構。文件說明類似「要執行自己的操作,只要建立 process 方法的子類別即可。但務必記得以呼叫父類別開始您的方法」。範例可能類似這樣。

public class EventHandler ...
  public void handle (BankingEvent e) {
    housekeeping(e);
  }
public class TransferEventHandler extends EventHandler...
  public void handle(BankingEvent e) {
    super.handle(e);
    initiateTransfer(e);
  }
    

每當您必須記得每次執行某項操作時,這表示 API 不佳。API 應該記住管理呼叫。執行此項操作的常見方式是將 handle 方法設為範本方法,如下所示。

public class EventHandler ...
  public void handle (BankingEvent e) {
    housekeeping(e);
    doHandle(e);
  }
  protected void doHandle(BankingEvent e) {
  }
public class TransferEventHandler extends EventHandler ...
  protected void doHandle(BankingEvent e) {
    initiateTransfer(e);
  }
    

在此,父類別定義公開方法,並提供一個獨立的方法(通常稱為掛勾方法)供子類別覆寫。子類別撰寫者現在不必擔心呼叫父類別的醜陋頭部。此外,父類別撰寫者可以自由地在子類別方法後新增呼叫(如果她願意)。

有幾種方式可以定義掛勾方法。在此情況下,我顯示一個空實作。如果許多子類別不需要提供自己的額外行為,這會很有用。如果許多子類別必須執行相同的工作,您可以考慮預設實作,它本身也可以是範本方法,以便在常見架構中允許變異。如果每個子類別都應該提供獨特的行為,則可以在父類別中將掛勾方法設為抽象。

其中一個問題是通常沒有辦法指出某個方法是掛勾方法,亦即它是架構撰寫者預期會被覆寫的方法。您確實會看到慣例(handle/doHandle 是常見的一個),但大多數時候您必須說明其運作方式。說明它的最佳方式之一就是透過範例。當我查看其中一個案例時,我通常發現最好的方法是查看現有的子類別並了解它的作用。

在此情況下,有一個單一且相對明顯的掛勾方法。然而,通常會有許多方法,所有方法都可能是掛勾,具體取決於子類別想要控制的程度。快速瀏覽我的 HTML 配置類別的抽象基底類別,會顯示六個可以覆寫的方法。有些子類別想要執行簡單的操作,並覆寫小型掛勾;其他子類別需要執行更花俏的操作,並覆寫範圍較大的方法。

在某些語言中,你可以使用 Seal 密封處理方法,以防止子類別覆寫它。由於我有一個 EnablingAttitude,所以我通常不願意這麼做,尤其是當繼承實際上是一個已發布的介面時。支持密封處理的論點是,這表示子類別無法中斷你的超級類別。我不認為覆寫錯誤的東西是「中斷超級類別」。子類別和超級類別必須密切合作,畢竟繼承是一種非常親密的關係。這件事不是成功就是失敗。密封處理可以是一種很好的方式,用來指出某人覆寫了錯誤的東西,因此我傾向於將它用於非發布介面(也就是說,子類別是同一個程式碼庫的一部分)。我對密封處理的問題是,子類別可能想要覆寫處理呼叫,以執行特別花俏的事情。我無法預測子類別的需求,所以我寧願說「繼續吧,但後果自負」,而不是「不行」。如果所有內容都是一個程式碼庫,那麼我們隨時可以取消密封處理。

多層級掛鉤

因此,我希望你能了解,你不應該依賴呼叫超級類別來處理這種情況。但是,當你有多個層級的架構時,就會產生複雜性。

在這裡,我將切換到一個真實的範例,一個時不時會出現的範例:JUnit。JUnit 使用範本方法來控制其測試案例的整體執行,它看起來像這樣

public abstract class TestCase
  public void runBare() throws Throwable {
    setUp();
    try {
      runTest();
    }
    finally {
      tearDown();
    }
  }
  protected void setUp() throws Exception {
  }
  protected void tearDown() throws Exception {
  }

它是一個熟悉的範本方法,有兩個用於覆寫的空掛鉤方法。到目前為止,一切都很好;你可以輕鬆地根據需要新增自己的設定和中斷程式碼。

當使用者想要從 JUnit 衍生另一個架構時,就會出現複雜性。那裡有很多範例,但讓我們舉一個簡單的案例,我們有一個專案特定的慣例,例如像這樣。

public class AlphaTestCase extends TestCase
   protected void setUp() throws Exception {
     alphaProjectSetup();
   }

這正是你會遇到呼叫超級類別問題的地方。因此,如果我們使用前面提到的建議,我們可以重新定義它,如下所示。

public class AlphaTestCase  extends TestCase...
  final protected void setUp() throws Exception {
    alphaProjectSetup();
    doSetUp();
  }    
  protected void doSetUp() throws Exception {        
  }

雖然這有效,但會遇到讓熟悉 JUnit 的人感到困惑的問題。他們參與的每個專案、他們讀過的每本書都說他們應該覆寫 setUp,而不是這個新奇的 doSetUp。這是一個使用 final 的好案例,因為人們感到困惑的機率很高。但是,即使使用 final,不同的設定方法所造成的混淆也會非常痛苦。

還有另一種選擇。第二層級架構可以覆寫 setUp 的呼叫者。

public class AlphaTestCase extends TestCase...
  public void runBare() throws Throwable {
    alphaProjectSetup();
    setUp();
    try {
      runTest();
    }
    finally {
      tearDown();
    }
  }
  protected void setUp() throws Exception {
  } 

現在每個人都可以像平常一樣使用 setUp。架構撰寫者也有更多選項,如果他們想要在有趣的地方加入其他行為。這總是一個值得考慮的選項 - 如果範本方法在你的地方無法運作,請考慮往上一個層級。

當然,沒有所謂的免費範本。在某些情況下,你無法這麼做,因為你不知道該怎麼做,因為沒有原始碼。在其他情況下,架構撰寫者有 DirectingAttitude,並封鎖呼叫方法,阻止你覆寫它。

即使你可以這麼做,也有些事情需要注意。如果主要的架構撰寫者需要變更你已覆寫的方法(就像 JUnit 在 2004 年 10 月 9 日所做的),該怎麼辦?像往常一樣,繼承的力量伴隨著責任,你必須注意超類別中發生的變更。

現在另一個可用的選項是使用 註解。JUnit 和其他基於 Java 的架構在過去幾個月中一直使用註解,遵循 NUnit 設定的領先地位。註解允許你提供比名稱更多的元資料給方法,讓你可以在這種情況下有更多選項。但那是另一天的主題了。