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に渡す

今のコードのtitletextを直接渡す方法では、 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.Sortsort.InterfaceというStrategyを取るようになっています。