2012/10/10

Go言語で Chain method を書いてみる

とりあえずチェーンメソッドを書いてみる

type Foo struct {
    buffer []byte
}

func (this *Foo) S(s string) *Foo {
    this.buffer = append(this.buffer, s...)
    return this
}

func (this *Foo) String() string {
    return string(this.buffer)
}

これは正常に動作する。

func Example001() {
    x := new(Foo)
    x.S("Hello, ").S("Chain method.")
    fmt.Println(x)
    // Output:
    // Hello, Chain method.
}

チェーンメソッドを持つクラスを継承して拡張すると面倒なことになる

先程の Foo をベースに byte を扱う B() メソッドを追加したクラス Boo を作成してみる。

type Boo struct {
    Foo
}

func (this *Boo) S(s string) *Boo {
    this.Foo.S(s)
    return this
}

func (this *Boo) B(b byte) *Boo {
    this.Foo.buffer = append(this.Foo.buffer, b)
    return this
}

一応できてる。

func Example002() {
    x := new(Boo)
    x.S("Hello, ").S("Chain method.").B('&').S("extend.")
    fmt.Println(x)
    // Output:
    // Hello, Chain method.&extend.
}

しかし、ここで面倒なのは S() メソッドを書き直してる点。 Foo は自身のポインタを返してチェーンメソッドを実現しているけど、 Foo クラスを拡張した Boo クラスはそのままでは *Foo を返すため、以降、B() メソッドを呼び出すことができない。 このため、*Boo を返すように S() メソッドを上書きする必要がある。

チェーンメソッドを持つクラスを interface 化すると実行速度が遅くなる

先程の Foo の別な実装を作成する場合、interface 化できると便利だ。 そこで、こんなコードに変えてみる。

// interface Chain
type Chain interface {
    S(s string) Chain
    String() string
}

// class Foo
type Foo struct {
    buffer []byte
}

func (this *Foo) S(s string) Chain {
    this.buffer = append(this.buffer, s...)
    return this
}

func (this *Foo) String() string {
    return string(this.buffer)
}

ポイントは

  • Chain interface を定義した点
  • Foo.S() が Chain interface を返している点

の2つ。

もちろん、このコードは正常に働く。

func Example003() {
    x := new(Foo)
    x.S("Hello, ").S("Chain method.")
    fmt.Println(x)
    // Output:
    // Hello, Chain method.
}

しかし、実行速度を計測してみると極端に実行速度が落ちていることがわかる。 いくつかのコードを足して、実行速度を計測してみる。 計測する Foo クラスのコードはこちら。

// class Foo
type Foo struct {
    buffer []byte
}

func (this *Foo) Reset() {
    this.buffer = this.buffer[0:0]
}

func (this *Foo) S(s string) Chain {
    this.buffer = append(this.buffer, s...)
    return this
}

func (this *Foo) S2(s string) *Foo {
    this.buffer = append(this.buffer, s...)
    return this
}

func (this *Foo) String() string {
    return string(this.buffer)
}

変更した点は、

  • 繰り返しテストするために Reset() メソッドを追加した。
  • 速度比較のために interface ではなく、*Foo を返す S2() メソッドを追加した。

計測コードはこちら。

const (
    benchmarkLoops = 100
)

var benchmarkDatas = []string{
    "Lorem ipsum dolor sit amet",
    "consectetur adipisicing elit",
    "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
    "Ut enim ad minim veniam",
    "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat",
}

// S() メソッドの速度を計測
func Benchmark001(b *testing.B) {
    buffer := new(Foo)

    for i := 0; i < b.N; i++ {
        buffer.Reset()
        for n := 0; n < benchmarkLoops; n++ {
            buffer.
                S(benchmarkDatas[0]).
                S(benchmarkDatas[1]).
                S(benchmarkDatas[2]).
                S(benchmarkDatas[3]).
                S(benchmarkDatas[4])
        }
    }
}

// S2() メソッドの速度を計測
func Benchmark002(b *testing.B) {
    buffer := new(Foo)

    for i := 0; i < b.N; i++ {
        buffer.Reset()
        for n := 0; n < benchmarkLoops; n++ {
            buffer.
                S2(benchmarkDatas[0]).
                S2(benchmarkDatas[1]).
                S2(benchmarkDatas[2]).
                S2(benchmarkDatas[3]).
                S2(benchmarkDatas[4])
        }
    }
}

結果は、以下の通り。

Benchmark001-8    100000         26795 ns/op
Benchmark002-8    100000         15888 ns/op

ウチの環境(Ubuntu 12.04 x64/go 1.0.3) では interface を返すメソッドを使用した Benchmark001 は、*Foo を返すメソッドを使用した Benchmark002 より10907 ns/op 遅いという結果がでた。 推測するに、Go言語では interface へのキャストをするとき、常に動的なキャストを実施しているためだと思う。 この辺りは、今後のGo言語の最適化の中で改善されるかもしれない。

Go言語にはチェーンメソッドは似合わない

拡張性、パフォーマンスの面からGo言語のチェーンメソッドの実装を見てみたけど、結論としては

  • Go言語にはチェーンメソッドは似合わない

と言えると思う。 もちろん、Go言語の仕様変更や、実装によっても変わるかもしれない。

0 件のコメント:

コメントを投稿