Goによるデザインパターン - Strategy パターン (2)
Strategy設計の失敗
前回で、HTMLとテキストでレポートを出力するコードを書きました。その中でFormatter interfaceを定義し、その具象型としてPlainTextFormatter HTMLFormatterを定義するというStrategyパターンを採用しました。
しかし、気になる点が無いではありません。
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{}
// (メソッドの実装は中略)
type HTMLFormatter struct{}
// (メソッドの実装は中略)
Formatter interface には6つのメソッドが含まれます。しかし、この切り分け方は適切だったのでしょうか? 6つというのは多すぎる気がします。また、今後新しいフォーマットを採用する際に、不備が発覚するかもしれません(フッタ―にもタイトルを出力したくなるかも!)。
Strategyパターンを使う際は、ストラテジの範囲と渡すべきデータを見極める必要があります。
ContextをStrategyに渡す
今のコードのtitleとtextを直接渡す方法では、
1. Strategyのどのメソッドがどのデータを必要とするか、覚えなくてはならない
2. 必要ないと思ったデータも実は必要になるかもしれない(フッタ―にもタイトルを出力)
3. 全く新しいデータが登場するかもしれない(日付や提出者も記載したい)
という問題があります。
そんな時は、Context(呼び出し側)をStrategyの引数として渡します。
Contextをそのまま渡すこともできますが(下のコードで言えば*report)、Contextもinterfaceにして渡した方がテストなどで便利になるでしょう(Report)。
// template_method.4.go package main import ( "fmt" "time" ) type Formatter interface { OutputStart(r Report) OutputHead(r Report) OutputBodyStart(r Report) OutputLine(r Report, line string) OutputBodyEnd(r Report) OutputEnd(r Report) } type Report interface { Title() string Text() []string Date() time.Time } type report struct { title string text []string date time.Time formatter Formatter } func (r *report) Title() string { return r.title } func (r *report) Text() []string { return r.text } func (r *report) Date() time.Time { return r.date } func (r *report) Output() { r.formatter.OutputStart(r) r.formatter.OutputHead(r) r.formatter.OutputBodyStart(r) for _, line := range r.text { r.formatter.OutputLine(r, line) } r.formatter.OutputBodyEnd(r) r.formatter.OutputEnd(r) } type HTMLFormatter struct{} func (*HTMLFormatter) OutputStart(r Report) { fmt.Println("<html>") } func (*HTMLFormatter) OutputHead(r Report) { fmt.Println("<head>") fmt.Printf("<title>%s</title>\n", r.Title()) fmt.Println("</head>") } func (*HTMLFormatter) OutputBodyStart(Report) { fmt.Println("<body>") } func (*HTMLFormatter) OutputLine(_ Report, line string) { fmt.Printf("<p>%s</p>\n", line) } func (*HTMLFormatter) OutputBodyEnd(r Report) { fmt.Printf("<p>Updated: %s</p>", r.Date().Format("2006-01-02 15:04:05")) fmt.Println("</body>") } func (*HTMLFormatter) OutputEnd(Report) { fmt.Println("</html>") } func main() { report := &report{ title: "月次報告", text: []string{"順調", "最高"}, formatter: &HTMLFormatter{}, } report.Output() }
Strategyの実例
標準ライブラリのsortパッケージは、Strategyパターンの一例です。
sort.Sortはsort.InterfaceというStrategyを取るようになっています。