引子:抄錯題目的考試#

大鳥小時候數學老師在黑板上抄題,因近視看錯題目,做得再對也得不了分。

標準化考試最大的好處:大家都做一樣的題目,特別是選擇或判斷題,最大化限制了答題者的發揮,結果只是 ABCD 的對錯。

抄題目就有可能抄錯——這正好說明了模板方法模式的精神。

第一版:每個學生一份完整試卷#

class TestPaperA
{
    public void TestQuestion1() { /* 題目 + 答案 b */ }
    public void TestQuestion2() { /* 題目 + 答案 c */ }
    public void TestQuestion3() { /* 題目 + 答案 a */ }
}

class TestPaperB
{
    public void TestQuestion1() { /* 同樣的題目 + 答案 d */ }
    public void TestQuestion2() { /* 同樣的題目 + 答案 b */ }
    public void TestQuestion3() { /* 同樣的題目 + 答案 a */ }
}

兩個類別幾乎完全相同,只有答案不同。

  • 老師若改題目,兩份都要改
  • 學生抄錯,全部出錯
  • 維護成本與錯誤風險都很高

第二版:抽象父類#

把題目寫在父類,子類只負責答案:

class TestPaper
{
    public void TestQuestion1() { /* 題目 1 */ }
    public void TestQuestion2() { /* 題目 2 */ }
    public void TestQuestion3() { /* 題目 3 */ }
}

class TestPaperA : TestPaper
{
    public new void TestQuestion1() { base.TestQuestion1(); Console.WriteLine("答案: b"); }
    public new void TestQuestion2() { base.TestQuestion2(); Console.WriteLine("答案: c"); }
    public new void TestQuestion3() { base.TestQuestion3(); Console.WriteLine("答案: a"); }
}

雖比第一版好,但子類裡仍有大量重複:每個方法都先 base.TestQuestionX()Console.WriteLine("答案: ?")

既然用了繼承,所有重複的程式碼都應該上升到父類,而不是讓每個子類重複。

第三版:模板方法模式#

把答案抽出為虛方法,由子類覆寫:

class TestPaper
{
    public void TestQuestion1()
    {
        Console.WriteLine("題目 1:玄鐵可能是 [ ] a. 球磨鑄鐵 b. 馬口鐵 c. 高速合金鋼 d. 碳素纖維");
        Console.WriteLine($"答案:{Answer1()}");
    }
    public void TestQuestion2() { /* 同樣套路 */ }
    public void TestQuestion3() { /* 同樣套路 */ }

    protected virtual string Answer1() => "";
    protected virtual string Answer2() => "";
    protected virtual string Answer3() => "";
}

class TestPaperA : TestPaper
{
    protected override string Answer1() => "b";
    protected override string Answer2() => "c";
    protected override string Answer3() => "a";
}

class TestPaperB : TestPaper
{
    protected override string Answer1() => "c";
    protected override string Answer2() => "a";
    protected override string Answer3() => "a";
}

客戶端利用多型:

TestPaper studentA = new TestPaperA();
studentA.TestQuestion1();
studentA.TestQuestion2();
studentA.TestQuestion3();

子類只填答案,題目與輸出格式統一在父類管理。新增學生只需新增一個子類覆寫答案即可。

模板方法模式#

模板方法模式(Template Method Pattern):定義一個操作中演算法的骨架,而將一些步驟延遲到子類別中。模板方法使得子類可以不改變一個演算法的結構即可重新定義該演算法的某些特定步驟。[DP]

結構#

  • AbstractClass:抽象模板,定義並實作一個模板方法(給出頂級邏輯的骨架),並宣告若干抽象操作交由子類實作
  • ConcreteClass:實作父類所定義的抽象方法,賦予不同實現
classDiagram
    class AbstractClass {
        <<abstract>>
        +TemplateMethod()
        +PrimitiveOperation1()*
        +PrimitiveOperation2()*
    }
    class ConcreteClassA {
        +PrimitiveOperation1()
        +PrimitiveOperation2()
    }
    class ConcreteClassB {
        +PrimitiveOperation1()
        +PrimitiveOperation2()
    }
    AbstractClass <|-- ConcreteClassA
    AbstractClass <|-- ConcreteClassB
abstract class AbstractClass
{
    public abstract void PrimitiveOperation1();
    public abstract void PrimitiveOperation2();

    public void TemplateMethod()
    {
        PrimitiveOperation1();
        PrimitiveOperation2();
        Console.WriteLine("");
    }
}

class ConcreteClassA : AbstractClass
{
    public override void PrimitiveOperation1() { ... }
    public override void PrimitiveOperation2() { ... }
}

何時使用?#

模板方法模式提供了一個很好的程式碼複用平台

當有一個由一系列步驟構成的過程,從高層次看相同,但部分步驟實現可能不同時,就應該考慮模板方法模式。

優勢:

  • 不變行為搬移到超類,去除子類中的重複程式碼
  • 把不變與可變的行為從子類中分離開
  • 讓子類專注於可變部分,不再受重複行為的糾纏

模板方法模式非常常用,對繼承與多型運用熟練的人幾乎都會在繼承體系中用到它。

例如 .NET 或 Java 類庫的設計中,通常會利用模板方法模式把公共行為提取到抽象類中。

注意事項#

模板方法只能消除「客觀題」式的重複——若是「主觀編程題」(步驟差異大、結構不同),就不適合用模板方法。

模式的選用,仍要看問題的本質結構是否真的有共通骨架。