第四部分:行为型模式 - 模板方法模式 (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. 总结
模板方法模式是一种基于继承的代码复用技术。它允许我们定义一个算法的骨架,并将算法中某些步骤的实现延迟到子类。这使得子类可以在不改变算法整体结构的前提下,重新定义算法的特定步骤。该模式在框架设计中非常常见,因为它提供了一种标准化的方式来构建可扩展的组件。通过合理使用抽象方法和钩子方法,可以在固定算法流程和提供灵活性之间找到一个很好的平衡点。