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を取るようになっています。