設計模式 in Go: Template Metho

行爲模式旨在解決對象間通信和交互相關的問題,專注於定義那些複雜到無法靜態設計的協作策略,這些協作策略使得程序可以在運行時動態地進行職責派遣以實現更好的擴展。

今天我們開始第 5 個行爲模式的學習 —— Template Method(模版方法)。

問題背景:

在設計一系列步驟的算法時,有些步驟可能在各個子類中都是通用的,而其他步驟則會有所不同。如果我們每個子類都實現所有步驟,將會產生大量重複代碼,從而增加代碼複雜性。

解決方案:

模板方法設計模式在一個方法中定義算法的骨架,將一些步驟推遲到子類中實現。它有助於封裝算法中的不變部分,並讓子類實現可變的部分。

因此,我們可以定義一個抽象基類,該基類指定一個骨架模板方法來定義算法步驟。一些步驟在基類中實現,而其他步驟則是要在子類中實現的抽象方法。

這裏有一個例子,我們想要爲金融和教育領域生成報告。我們需要收集數據、分析數據、最終確定並生成最終報告。對於這兩種類型的報告,我們將生成不同的標題。

實例代碼:

package templatemethod

import"fmt"

type Reporterinterface {
 initializeReport()
 collectData()
 analyze()
 finalize()
 finalReport()
}

// Abstract base class
type baseReportstruct {
}

funcGenerateReport(rr Reporter) {
 rr.initializeReport()
 rr.collectData()
 rr.analyze()
 rr.finalize()
 rr.finalReport()
}

// Base class steps implemented
func(r *baseReport) initializeReport() {
 fmt.Println("Initializing report...")
}

func(r *baseReport) collectData() {
 fmt.Println("Collecting data...")
}

func(r *baseReport) analyze() {
 fmt.Println("Analyzing report data...")
}

func(r *baseReport) finalize() {
panic("not implemented")
}

func(r *baseReport) finalReport() {
panic("not implemented")
}

funcNewFinancialReport() *FinancialReport {
return &FinancialReport{}
}

// Concrete sub-class
type FinancialReportstruct {
 baseReport// Embedded Report
}

// Override finalize method
func(f *FinancialReport) finalize() {
 fmt.Println("💲Finanical finalize")
}

// Override hook method
func(f *FinancialReport) finalReport() {
 fmt.Println("💲Generating financial report")
}

funcNewEducationReport() *EducationReport {
return &EducationReport{}
}

// Concrete sub-class
type EducationReportstruct {
 baseReport// Embedded Report
}

// Override finalize method
func(r *EducationReport) finalize() {
 fmt.Println("📚 Education finalize")
}

// Override hook method
func(r *EducationReport) finalReport() {
 fmt.Println("📚 Generating education report")
}

運行測試以演示該模式:

package templatemethod

import (
"fmt"
"testing"
)

funcTestTemplateMethod(t *testing.T) {

 fmt.Println("----- financial report -----")
 r1 := NewFinancialReport()
 GenerateReport(r1)

 fmt.Println("----- education report -----")
 r2 := NewEducationReport()
 GenerateReport(r2)
}
Running tool: /opt/go/bin/gotest -timeout 30s -run ^TestTemplateMethod$ templatemethod -v -count=1

=== RUN TestTemplateMethod
----- financial report -----
Initializing report...
Collecting data...
Analyzing report data...
💲Finanical finalize
💲Generating financial report
----- education report -----
Initializing report...
Collecting data...
Analyzing report data...
📚 Education finalize
📚 Generating education report
--- PASS: TestTemplateMethod (0.00s)
PASS
ok templatemethod 0.001s

ps:此處我們優先考慮可讀性,不會太關注編碼標準,如註釋、camelCase 類型名等。我們將多個文件的代碼組織到一個 codeblock 中僅僅是爲了方便閱讀,如果您想測試可以通過 git 下載源碼 github.com/hitzhangjie/go-patterns。

本文總結:

模板方法模式在一個基類中提供了一個算法的骨架結構,並將一些步驟推遲到子類中實現。這允許子類在不改變算法整體結構的情況下定製特定步驟的行爲。通過這種方式,模板方法模式確保了算法的整體框架保持一致,同時提供了靈活性以適應不同的需求和實現細節。

Warning:組合優於繼承,但組合不是繼承

在使用模板方法模式(Template Method Pattern)實現時,一個常見的誤解是在 Go 語言中將 GenerateReport 定義爲接口方法,並在 baseReport 類型中實現它。這種做法可能導致設計上的問題和限制。

爲了更清晰地解釋爲什麼這並不是一個好的實踐,我們重寫部分代碼:

// if we define template method as interface method
type Reporter interface {
 GenerateReport()
 ...
}

// then we implement this method in baseReport type
type baseReport struct {}

func(br *baseReport) GenerateReport() {
 br.initializeReport()
 br.collectData()
 br.analyze()
 br.finalize()// <= this is a mistake!
 br.finalReport()// <= this is a mistake!
}

type FinancialReport struct {
 baseReport
}

// then we want to overwrite the baseReport.finalize() and baseReport.finalReport()
func(fr *FinancialReport) finalize() { ... }
func(fr *FinancialReport) finaleport() { ... }

// then we want to call this template method
var r Reporter = &FinancialReport{}
r.GenerateReport()

在 Go 語言設計中,更傾向於使用組合(composition)而不是繼承。至於爲什麼,前面的文章中我們已經提過了,這裏不再贅述。實際上,在 Go 編程中,沒有像 Java 那樣的抽象類。Go 語言只有接口和具體的實現類。

在這裏,baseReport並不是FinancialReport的基類。儘管FinancialReport嵌入了baseReport,這其實是組合而非繼承。當調用baseReport.GenerateReport()時,會調用baseReport.finalize()baseReport.finalReport()方法,而不是在FinancialReport中新定義的方法。

這是一個對組合、繼承實現不清楚的開發者比較容易犯的錯誤。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/uN3DLinXiXde9w477BntQw