第四部分:行為型模式 - 模板方法模式 (Template Method Pattern)
現在我們來學習模板方法模式。這個模式在一個方法中定義一個算法的骨架,而將一些步驟延遲到子類中實現。模板方法使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
- 核心思想:定義一個操作中的算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。
模板方法模式 (Template Method Pattern)
“定義一個操作中的算法的骨架(骨架步驟的順序),而將一些步驟(具體實現)延遲到子類中。模板方法使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。” (Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.)
想象一下制作一杯飲料(比如咖啡或茶)的過程:
- 把水燒開 (Boil Water) - 這是一個通用步驟。
- 沖泡 (Brew) - 咖啡是沖泡咖啡粉,茶是浸泡茶葉。這是可變步驟。
- 把飲料倒進杯子 (Pour in Cup) - 這是一個通用步驟。
- 加調料 (Add Condiments) - 咖啡可能加糖和牛奶,茶可能加檸檬。這是可變步驟,并且可能是可選的。
模板方法模式就是把這個制作流程(算法骨架)定義在一個抽象類(比如 BeverageMaker
)的模板方法(比如 prepareBeverage()
)中:
prepareBeverage() {boilWater(); // 具體方法,父類實現brew(); // 抽象方法,子類實現pourInCup(); // 具體方法,父類實現if (customerWantsCondiments()) { // 鉤子方法,子類可覆蓋addCondiments(); // 抽象方法,子類實現}
}
boilWater()
和pourInCup()
是所有飲料制作共通的,可以在父類中直接實現。brew()
和addCondiments()
是具體飲料不同的,聲明為抽象方法,由子類(如CoffeeMaker
、TeaMaker
)去實現。customerWantsCondiments()
是一個“鉤子 (hook)”方法,父類可以提供一個默認實現(比如默認需要調料),子類可以覆蓋它來改變算法的某個特定點(比如某個子類飲料默認不需要調料,或者提供更復雜的判斷邏輯)。
這樣,算法的整體結構(燒水 -> 沖泡 -> 倒杯 -> 可能加調料)被固定下來了,但具體的沖泡內容和調料可以由子類自由定義。
1. 目的 (Intent)
模板方法模式的主要目的:
- 定義算法骨架:在一個抽象類中定義一個算法的框架,明確算法的執行步驟和順序。
- 延遲實現可變步驟:將算法中可變的步驟的實現推遲到子類中。
- 代碼復用:將算法中不變的部分(公共步驟)放在父類中,避免在子類中重復代碼。
- 控制子類擴展:父類通過模板方法控制算法的整體流程,子類只能在指定的可變點上進行擴展,而不能改變算法的結構。
2. 生活中的例子 (Real-world Analogy)
-
食譜 (Recipe):
- 模板方法:一道菜的烹飪流程(準備食材、切菜、炒制、調味、裝盤)。
- 具體步驟:不同的菜肴在“準備食材”、“炒制方法”、“調味料”等方面有不同的實現。
-
房屋建造流程:
- 模板方法:打地基 -> 建墻體 ->封頂 -> 內部裝修 -> 外部裝修。
- 具體步驟:不同的房屋類型(別墅、公寓)在“墻體材料”、“裝修風格”等方面有不同的實現。
-
軟件開發生命周期 (SDLC):
- 模板方法:需求分析 -> 設計 ->編碼 -> 測試 -> 部署 -> 維護。
- 具體步驟:不同的項目或團隊可能在“設計方法論”(如敏捷、瀑布)、“測試策略”等方面有不同的具體做法。
-
簡歷模板:
- 模板方法:個人信息 -> 教育背景 -> 工作經歷 -> 項目經驗 -> 技能證書。
- 具體步驟:每個人填寫的具體內容不同。
3. 結構 (Structure)
模板方法模式通常包含以下角色:
-
AbstractClass (抽象類):
- 定義一個或多個抽象操作(
primitiveOperation
),由具體子類實現這些操作。 - 定義一個模板方法(
templateMethod
),該方法實現了一個算法的骨架。模板方法會調用抽象操作、具體操作以及鉤子操作。 - 可以包含一些具體操作(
concreteOperation
),這些操作對所有子類都是相同的。 - 可以包含鉤子操作(
hookOperation
),通常在抽象類中提供一個默認實現(或者為空實現),子類可以根據需要覆蓋它們來影響算法的流程。
- 定義一個或多個抽象操作(
-
ConcreteClass (具體類):
- 繼承自 AbstractClass。
- 實現父類中定義的抽象操作。
- 可以覆蓋父類中的鉤子操作。
模板方法中的操作類型:
- 具體操作 (Concrete Operations):在抽象類中實現,子類可以直接使用或繼承。
- 抽象操作 (Abstract Operations / Primitive Operations):在抽象類中聲明(通常為抽象方法),必須由子類實現。
- 鉤子操作 (Hook Operations):在抽象類中提供一個默認實現(可能是空實現)。子類可以選擇性地覆蓋這些方法,以在算法的特定點“掛鉤”并改變算法的行為。鉤子常用于:
- 讓子類能夠對算法的某一步進行可選的擴展。
- 讓子類能夠決定算法的某一步是否執行。
模板方法通常被聲明為 final
(Java) 或在 Go 中通過非導出方法和導出方法組合的方式(父類調用非導出的可被子類“覆蓋”的方法)來防止子類改變算法的整體結構。
4. 適用場景 (When to Use)
- 當你想一次性實現一個算法的不變部分,并將可變的行為留給子類來實現時。
- 當各個子類中公共的行為應被提取出來并集中到一個公共父類中以避免代碼重復時。首先識別現有代碼中的不同之處,然后將不同之處分離為新的操作。最后,用一個調用這些新操作的模板方法來替換這些不同的代碼。
- 當需要控制子類的擴展時。模板方法只在特定點調用鉤子操作,這就允許在這些點進行擴展,而不是在其他地方。
- 框架開發:框架通常定義了整體的執行流程(模板方法),而應用程序開發者通過繼承框架中的類并實現特定的抽象方法或鉤子方法來定制應用行為。
5. 優缺點 (Pros and Cons)
優點:
- 代碼復用:將公共代碼放在抽象父類中,提高了代碼復用性。
- 封裝不變部分,擴展可變部分:算法的骨架(不變部分)在父類中定義和控制,可變部分由子類實現,易于擴展。
- 符合開閉原則:對擴展開放(可以通過增加子類來實現新的行為),對修改關閉(不需要修改父類的模板方法)。
- 控制反轉 (Inversion of Control - IoC):父類調用子類的操作,而不是子類調用父類。這是一種簡單的 IoC 形式,有時被稱為“好萊塢原則”——“不要給我們打電話,我們會給你打電話 (Don’t call us, we’ll call you)”。
缺點:
- 類的數量增加:每個不同的算法變體都需要一個單獨的子類,可能會導致類的數量增多。
- 繼承的限制:模板方法模式基于繼承,這帶來了一些固有的限制。例如,子類必須繼承抽象父類,如果子類已經有了其他父類(在不支持多重繼承的語言中),則無法使用此模式。
- 算法骨架固定:算法的整體流程由父類固定,如果需要對流程本身進行大的改動,可能會比較困難,可能需要修改父類的模板方法。
- 可讀性可能降低:如果模板方法中的步驟過多,或者鉤子方法的使用比較復雜,可能會使得理解算法的整體流程和子類的具體實現之間的關系變得困難。
6. 實現方式 (Implementations)
讓我們以制作不同類型的報告(例如,文本報告和HTML報告)為例。報告生成的流程可能包括:初始化、格式化頭部、格式化主體內容、格式化尾部、輸出。
抽象類 (ReportGenerator)
// report_generator.go (AbstractClass and its methods)
package templateimport "fmt"// ReportGenerator 抽象類 (通過接口和嵌入結構體模擬)
// Go 中沒有直接的抽象類,通常用接口定義行為,用結構體嵌入實現通用部分
// 或者定義一個包含未實現方法的結構體,讓具體子類去“填充”這些方法。
// 這里我們采用一種更接近傳統模板方法模式的結構:
// 一個包含模板方法的結構體,它調用由嵌入的“子類”接口實現的方法。// ReportSteps 定義了子類需要實現的步驟
type ReportSteps interface {initialize()formatHeader() stringformatBody() stringformatFooter() stringoutputReport(header, body, footer string)hookBeforeBody() // 鉤子方法
}// BaseReportGenerator 包含模板方法和通用邏輯
type BaseReportGenerator struct {steps ReportSteps // 指向具體實現這些步驟的對象 (子類)
}// NewBaseReportGenerator 構造函數,需要傳入具體的步驟實現者
func NewBaseReportGenerator(steps ReportSteps) *BaseReportGenerator {return &BaseReportGenerator{steps: steps}
}// GenerateReport 模板方法
func (rg *BaseReportGenerator) GenerateReport() {rg.steps.initialize() // 調用子類實現的初始化header := rg.steps.formatHeader() // 調用子類實現的頭部格式化rg.steps.hookBeforeBody() // 調用鉤子方法body := rg.steps.formatBody() // 調用子類實現的身體格式化footer := rg.steps.formatFooter() // 調用子類實現的尾部格式化rg.steps.outputReport(header, body, footer) // 調用子類實現的輸出fmt.Println("Report generation complete.")
}// --- 為了讓具體類能調用到BaseReportGenerator的方法,或者讓BaseReportGenerator能調用具體類的方法
// Go 的實現方式與 Java/C++ 的繼承有區別。一種常見做法是具體類持有 BaseReportGenerator,
// 或者 BaseReportGenerator 持有具體步驟的實現者(如上面的 ReportSteps)。
// 下面的具體類將實現 ReportSteps 接口。
// ReportGenerator.java (AbstractClass)
package com.example.template;public abstract class ReportGenerator {// Template method - final to prevent overriding the algorithm structurepublic final void generateReport() {initialize(); // Common step, or could be abstract/hookString header = formatHeader(); // Step to be implemented by subclasshookBeforeBody(); // Hook methodString body = formatBody(); // Step to be implemented by subclassString footer = formatFooter(); // Step to be implemented by subclassoutputReport(header, body, footer); // Common step, or could be abstract/hookSystem.out.println("Report generation complete.");}// Common methods (can be overridden if not final)protected void initialize() {System.out.println("ReportGenerator: Initializing common report data...");}protected void outputReport(String header, String body, String footer) {System.out.println("ReportGenerator: --- Final Report ---");System.out.println(header);System.out.println(body);System.out.println(footer);System.out.println("ReportGenerator: --- End of Report ---");}// Abstract methods (primitive operations) - to be implemented by subclassesprotected abstract String formatHeader();protected abstract String formatBody();protected abstract String formatFooter();// Hook method - subclass can override, but not mandatoryprotected void hookBeforeBody() {// Default implementation does nothingSystem.out.println("ReportGenerator: (Hook) No specific action before body by default.");}
}
具體類 (TextReportGenerator, HtmlReportGenerator)
// text_report_generator.go (ConcreteClass)
package templateimport "fmt"// TextReportGenerator 具體類
type TextReportGenerator struct {// 可以嵌入 BaseReportGenerator 來繼承其方法,但Go的模板方法通常不這么做// 或者讓 BaseReportGenerator 持有 TextReportGenerator 的實例 (通過 ReportSteps 接口)data string // 示例數據
}func NewTextReportGenerator(data string) *TextReportGenerator {return &TextReportGenerator{data: data}
}// 實現 ReportSteps 接口
func (tr *TextReportGenerator) initialize() {fmt.Println("TextReport: Initializing text report specific data...")
}func (tr *TextReportGenerator) formatHeader() string {return "=== TEXT REPORT HEADER ==="
}func (tr *TextReportGenerator) formatBody() string {return fmt.Sprintf("Body: %s\n(Rendered as plain text)", tr.data)
}func (tr *TextReportGenerator) formatFooter() string {return "--- TEXT REPORT FOOTER ---"
}func (tr *TextReportGenerator) outputReport(header, body, footer string) {fmt.Println("TextReport: Outputting to console:")fmt.Println(header)fmt.Println(body)fmt.Println(footer)
}func (tr *TextReportGenerator) hookBeforeBody() {fmt.Println("TextReport: (Hook) Adding a small note before text body.")
}// html_report_generator.go (ConcreteClass)
package templateimport "fmt"// HtmlReportGenerator 具體類
type HtmlReportGenerator struct {data string
}func NewHtmlReportGenerator(data string) *HtmlReportGenerator {return &HtmlReportGenerator{data: data}
}func (hr *HtmlReportGenerator) initialize() {fmt.Println("HtmlReport: Initializing HTML report specific data (e.g., CSS links)...")
}func (hr *HtmlReportGenerator) formatHeader() string {return "<html>\n<head><title>HTML Report</title></head>\n<body>\n <h1>HTML Report Header</h1>"
}func (hr *HtmlReportGenerator) formatBody() string {return fmt.Sprintf(" <p>Body: %s</p>\n <em>(Rendered as HTML)</em>", hr.data)
}func (hr *HtmlReportGenerator) formatFooter() string {return " <hr/>\n <footer>HTML Report Footer</footer>\n</body>\n</html>"
}func (hr *HtmlReportGenerator) outputReport(header, body, footer string) {fmt.Println("HtmlReport: Simulating saving to an HTML file:")fmt.Println(header)fmt.Println(body)fmt.Println(footer)
}func (hr *HtmlReportGenerator) hookBeforeBody() {// HTML報告可能不需要這個鉤子,或者有不同的實現fmt.Println("HtmlReport: (Hook) Adding a meta tag or script before HTML body.")
}
// TextReportGenerator.java (ConcreteClass)
package com.example.template;public class TextReportGenerator extends ReportGenerator {private String data;public TextReportGenerator(String data) {this.data = data;}@Overrideprotected void initialize() {super.initialize(); // Call common initialization if neededSystem.out.println("TextReport: Initializing text report specific data...");}@Overrideprotected String formatHeader() {return "=== TEXT REPORT HEADER ===";}@Overrideprotected String formatBody() {return "Body: " + this.data + "\n(Rendered as plain text)";}@Overrideprotected String formatFooter() {return "--- TEXT REPORT FOOTER ---";}@Overrideprotected void outputReport(String header, String body, String footer) {// Override if text report needs a different output mechanismSystem.out.println("TextReport: Outputting to console:");System.out.println(header);System.out.println(body);System.out.println(footer);}@Overrideprotected void hookBeforeBody() {System.out.println("TextReport: (Hook) Adding a small note before text body.");}
}// HtmlReportGenerator.java (ConcreteClass)
package com.example.template;public class HtmlReportGenerator extends ReportGenerator {private String data;public HtmlReportGenerator(String data) {this.data = data;}@Overrideprotected String formatHeader() {return "<html>\n<head><title>HTML Report</title></head>\n<body>\n <h1>HTML Report Header</h1>";}@Overrideprotected String formatBody() {return " <p>Body: " + this.data + "</p>\n <em>(Rendered as HTML)</em>";}@Overrideprotected String formatFooter() {return " <hr/>\n <footer>HTML Report Footer</footer>\n</body>\n</html>";}// We can use the default initialize() and outputReport() from ReportGenerator// or override them if specific HTML logic is needed.// Override hook if needed@Overrideprotected void hookBeforeBody() {System.out.println("HtmlReport: (Hook) Adding a meta tag or script before HTML body.");}
}
客戶端使用
// main.go (示例用法)
/*
package mainimport ("./template""fmt"
)func main() {fmt.Println("--- Generating Text Report ---")textData := "This is the data for the text report."textReportSteps := template.NewTextReportGenerator(textData)textReportGenerator := template.NewBaseReportGenerator(textReportSteps)textReportGenerator.GenerateReport()fmt.Println("\n--- Generating HTML Report ---")htmlData := "This is the data for the HTML report."htmlReportSteps := template.NewHtmlReportGenerator(htmlData)htmlReportGenerator := template.NewBaseReportGenerator(htmlReportSteps)htmlReportGenerator.GenerateReport()
}
*/
// Main.java (示例用法)
/*
package com.example;import com.example.template.HtmlReportGenerator;
import com.example.template.ReportGenerator;
import com.example.template.TextReportGenerator;public class Main {public static void main(String[] args) {System.out.println("--- Generating Text Report ---");String textData = "This is the data for the text report.";ReportGenerator textReport = new TextReportGenerator(textData);textReport.generateReport();System.out.println("\n--- Generating HTML Report ---");String htmlData = "This is the data for the HTML report.";ReportGenerator htmlReport = new HtmlReportGenerator(htmlData);htmlReport.generateReport();}
}
*/
7. 總結
模板方法模式是一種基于繼承的代碼復用技術。它允許我們定義一個算法的骨架,并將算法中某些步驟的實現延遲到子類。這使得子類可以在不改變算法整體結構的前提下,重新定義算法的特定步驟。該模式在框架設計中非常常見,因為它提供了一種標準化的方式來構建可擴展的組件。通過合理使用抽象方法和鉤子方法,可以在固定算法流程和提供靈活性之間找到一個很好的平衡點。