例:レポートを出力するクラス。
レポートをHTMLで出力するstruct
*1を作ったあなた。上司からプレーンテキストでも出力してくれと言われてしまいました。とりあえず、フォーマットを引数で指定するようにしたのですが・・・
// template_method.1.go package main import ( "fmt" ) type Report struct { Title string Text []string } func (r *Report) OutputReport(format string) error { if format == "plain" { fmt.Printf("*** %s ***\n", r.Title) } else if format == "html" { fmt.Println("<html>") fmt.Println("<head>") fmt.Printf("<title>%s</title>\n", r.Title) fmt.Println("</head>") fmt.Println("<body>") } else { return fmt.Errorf("unknown format: %s", format) } for _, line := range r.Text { if format == "plain" { fmt.Println(line) } else { fmt.Printf("<p>%s</p>\n", line) } } if format == "html" { fmt.Println("</body>") fmt.Println("</html>") } return nil } func main() { report := Report{ Title: "月次報告", Text: []string{"順調", "最高"}, } report.OutputReport("plain") report.OutputReport("html") }
問題点
このプログラムの悪い点は、OutputReportの中でHTML固有の処理とプレーンテキスト固有の処理が絡み合っていること。もしも、さらにCSV、PDF、等々とフォーマットが増えていったら・・・やってられません!
この状況は、デザインパターンの原則「変わるものを変わらないものから分離する」に反しています。
解決法「Template Method パターン」・・・あれっ?継承が無いぞ!?
上のソースでは、HTMLもプレーンテキストも「Reportに格納されたタイトルと本文を出力する」という処理は変わりません。
もしこれがRubyならば、抽象基底クラスを定義してフォーマット毎の処理はサブクラスに任せるTemplate methodパターンを使うところですが、Goに継承はありません!
関数を使ったStrategyパターン
そこで、フォーマット毎の処理をサブクラスではなく、関数に移譲します。Strategyパターンです。
// template_method.2.go package main import ( "fmt" ) type Report struct { Title string Text []string Formatter func(title string, text []string) } func (r *Report) Output() { r.Formatter(r.Title, r.Text) } func FormatPlainText(title string, text []string) { fmt.Printf("*** %s ***\n", title) for _, line := range text { fmt.Println(line) } } func FormatHTML(title string, text []string) { fmt.Println("<html>") fmt.Println("<head>") fmt.Printf("<title>%s</title>\n", title) fmt.Println("</head>") fmt.Println("<body>") for _, line := range text { fmt.Printf("<p>%s</p>\n", line) } fmt.Println("</body>") fmt.Println("</html>") } func main() { report := Report{ Title: "月次報告", Text: []string{"順調", "最高"}, Formatter: FormatPlainText, } report.Output() report.Formatter = FormatHTML report.Output() }
このとき、FormatHTML
とFormatPlainText
はどちらもそれぞれ、「レポートをフォーマットする」というストラテジStrategyを定義しています。一方、Strategyを使う側のReport
をContextと呼びます。
「レポートをフォーマットする」機能の実装をformatterに任せて、Reportからその部分を取り除くことで「関心の分離」を行うことが出来ます。
オブジェクトによるStrategyパターン
上のコードでは、フォーマット処理全体を1個の関数にしましたが、FormatHTML
もFormatPlainText
も、
ヘッダ情報を出力
↓
タイトルを出力
↓
本文を出力
↓
末尾の部分を出力
という処理の流れは変わりません。 そのため、フォーマット処理全体を切り替えるのではなく、フォーマット毎に異なる部分だけを移譲した方が、 不変な部分を再実装する必要がなくなります。
そこで、関数ではなく、オブジェクトを渡すようにします。
// template_method.3.go package main import ( "fmt" ) type Formatter interface { OutputStart() OutputHead(text string) OutputBodyStart() OutputLine(line string) OutputBodyEnd() OutputEnd() } type Report struct { Title string Text []string Formatter Formatter } func (r *Report) Output() { r.Formatter.OutputStart() r.Formatter.OutputHead(r.Title) r.Formatter.OutputBodyStart() for _, line := range r.Text { r.Formatter.OutputLine(line) } r.Formatter.OutputBodyEnd() r.Formatter.OutputEnd() } type PlainTextFormatter struct{} func (*PlainTextFormatter) OutputStart() { } func (*PlainTextFormatter) OutputHead(title string) { fmt.Printf("*** %s ***\n", title) } func (*PlainTextFormatter) OutputBodyStart() { } func (*PlainTextFormatter) OutputLine(line string) { fmt.Println(line) } func (*PlainTextFormatter) OutputBodyEnd() { } func (*PlainTextFormatter) OutputEnd() { } type HTMLFormatter struct{} func (*HTMLFormatter) OutputStart() { fmt.Println("<html>") } func (*HTMLFormatter) OutputHead(title string) { fmt.Println("<head>") fmt.Printf("<title>%s</title>\n", title) fmt.Println("</head>") } func (*HTMLFormatter) OutputBodyStart() { fmt.Println("<body>") } func (*HTMLFormatter) OutputLine(line string) { fmt.Printf("<p>%s</p>\n", line) } func (*HTMLFormatter) OutputBodyEnd() { fmt.Println("</body>") } func (*HTMLFormatter) OutputEnd() { fmt.Println("</html>") } func main() { report := Report{ Title: "月次報告", Text: []string{"順調", "最高"}, Formatter: &PlainTextFormatter{}, } report.Output() report.Formatter = &HTMLFormatter{} report.Output() }
ごく単純なストラテジーの場合は関数で十分なこともありますが、多くの場合はinterface
を定義した方がよいでしょう。
*1:レポートの内容はフィクションです