読者です 読者をやめる 読者になる 読者になる

Goによるデザインパターン - Strategy パターン (1)

Go デザインパターン Goによるデザインパターン

例:レポートを出力するクラス。

レポートを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()
}

このとき、FormatHTMLFormatPlainTextはどちらもそれぞれ、「レポートをフォーマットする」というストラテジStrategyを定義しています。一方、Strategyを使う側のReportContextと呼びます。

「レポートをフォーマットする」機能の実装をformatterに任せて、Reportからその部分を取り除くことで「関心の分離」を行うことが出来ます。

オブジェクトによるStrategyパターン

上のコードでは、フォーマット処理全体を1個の関数にしましたが、FormatHTMLFormatPlainTextも、

ヘッダ情報を出力

   ↓

タイトルを出力

   ↓

本文を出力

   ↓

末尾の部分を出力

という処理の流れは変わりません。 そのため、フォーマット処理全体を切り替えるのではなく、フォーマット毎に異なる部分だけを移譲した方が、 不変な部分を再実装する必要がなくなります。

そこで、関数ではなく、オブジェクトを渡すようにします。

// 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:レポートの内容はフィクションです