7.11章にデコレータパターンによる振る舞いの多層化と型の検知に関する注意書きがある。
インタフェースの実装では、Decoratorパターンを使って同じインタフェースの実装をラップし、振る舞いを多層化するのが一般的です。ところがラップされた実装の中でオプションのインタフェースが実装されていた場合には、型アサーションや型Switchではそれが検知できないのです。たとえば標準ライブラリにはバッファ付きで読む機能を提供するパッケージbufioが含まれています。io.Readerの実装を関数bufio.NewReaderに渡して、返される*bufio.Readerを使うことで、どんなio.Readerでもバッファ付きにすることができます。しかし、渡されたio.Readerがio.ReaderFromも実装していると、それをバッファ付きにラップしても最適化できません
これについて具体的なコードを下に考察していく。
前提としてio.CopyがWriterToを実装していることを検知できた場合はそれを使ってコピーを行うことが前段で述べられている。
これがナイーブにDecoratorパターンを使った場合に検知できなくなるコードを以下に書いてみる
package main
import (
"bytes"
"fmt"
"io"
)
// 元のソース:Reader でもあり、WriterTo も実装(= io.Copy が速くなる)
type fastSrc struct {
data []byte
writeToWasUsed bool // 監視用フラグ
pos int
}
func (s *fastSrc) Read(p []byte) (int, error) {
if s.pos >= len(s.data) {
return 0, io.EOF
}
n := copy(p, s.data[s.pos:])
s.pos += n
return n, nil
}
func (s *fastSrc) WriteTo(w io.Writer) (int64, error) { // ★最適化経路
s.writeToWasUsed = true
n, err := w.Write(s.data[s.pos:])
s.pos += n
return int64(n), err
}
// ただの Reader デコレータ(ログなどを付ける想定)
type loggingReader struct{ r io.Reader }
func (l loggingReader) Read(p []byte) (int, error) {
n, err := l.r.Read(p)
// ここでログ出力などをする想定
_ = n
return n, err
}
// 書き捨て先(ReaderFrom は実装しない)
type sink struct{ n int64 }
func (s *sink) Write(p []byte) (int, error) { s.n += int64(len(p)); return len(p), nil }
func main() {
payload := bytes.Repeat([]byte("x"), 1<<20) // 1MB
// A) 直で渡す → WriterTo が使われる
a := &fastSrc{data: payload}
dstA := &sink{}
_, _ = io.Copy(dstA, a)
fmt.Println("A: WriterTo used?", a.writeToWasUsed) // => true(高速経路)
// B) ラップして渡す → 外側は「ただの io.Reader」なので検知されない
b := &fastSrc{data: payload}
wrapped := loggingReader{r: b}
dstB := &sink{}
_, _ = io.Copy(dstB, wrapped)
fmt.Println("B: WriterTo used?", b.writeToWasUsed) // => false(汎用ループにフォールバック)
}
このように不用意にラップしてしまうことで、高速化のための仕組みを潰してしまうことになりうる。
このような状況が発生した場合は、ラッパも「オプションのインタフェース」を再実装することで高速化することを検討する
// 改良版デコレータ:WriterTo を“露出”して内側に委譲
type loggingReader2 struct{ r io.Reader }
func (l loggingReader2) Read(p []byte) (int, error) { return l.r.Read(p) }
// ここが重要:中身が WriterTo ならそこへ委譲
func (l loggingReader2) WriteTo(w io.Writer) (int64, error) {
if wt, ok := l.r.(io.WriterTo); ok {
return wt.WriteTo(w) // 内側の高速経路を活かす
}
// それが無いなら自分で素直にコピー
return io.Copy(w, l.r)
}
func demoFix() {
c := &fastSrc{data: bytes.Repeat([]byte("y"), 1<<20)}
dst := &sink{}
_, _ = io.Copy(dst, loggingReader2{r: c})
fmt.Println("C: WriterTo used?", c.writeToWasUsed) // => true(委譲のおかげで最適化が生きる)
}