呼叫父類
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 設定的領先地位。註解允許你提供比名稱更多的元資料給方法,讓你可以在這種情況下有更多選項。但那是另一天的主題了。