Go のオブジェクト指向

OOP におけるインタフェースとは、再利用される側に課せられる義務を表現したものです。この義務を完遂して実装を提供すれば、きちんと再利用してもらうことが期待できます。そのためにはまず、これから実装しようとするインタフェースの目的・意図をしっかり理解する必要があります。そして実装すべきインタフェースを宣言するのです。こうしてインタフェースと実装が結び付き、再利用の準備が整います。

ところが Go では宣言的にインタフェースを実装することができません。このため、あるインタフェースに適合するかどうかは、インタフェースの定義するメソッドが実装されているかどうかだけで判断されます。

具体例を挙げてみます。同一のメソッドセットを持つインタフェース A と B があるとします。ここで、A と B はそれぞれ異なる目的があるとします。そこで型 C は意図して A を実装するとします。

// a.go
type A interface { Foo() int; }

// b.go
type B interface { Foo() int; }

// c.go
type C struct { };
func (c *C) Foo() int { return 0; }

C は A を実装したつもりですが、その意図をコンパイラに伝達することができません。コンパイラはただ単に、C が A や B として再利用される箇所で、インタフェースを満たしているかどうかをチェックするだけです。しかし、C の実装する Foo は B として再利用されることを想定していない可能性があります。それでも C を B として使うことができてしまう。その結果どんなことが起きるかは、やってみるまでわかりません。

これは稀なケースだと思います。しかし、こういった意図せぬ再利用を回避する方法が用意されていないのです。

もちろん、こういったトラブルに手も足も出ないわけではありません。ランタイムでおかしなことが起き、その原因が意図せぬ再利用にあるとわかった時の対応はおそらくその型の別名を定義し、メソッドを上書きすることです。

type C2 C;
func (c2 *C2) Foo() int { return 1; }

事前の回避はできなくとも、事後のパッチでいくらでも逃げることができるわけです。インタフェースの衝突が稀であれば、パッチが必要なケースも減るので、コストはそれほど高くつかない。TDD をしっかり回せるプロジェクトなら「あり」の考え方かもしれません。なにせ、コンパイルも速い。でも、こういったトラブルは発見しにくいような気がします。現象からそれが意図せぬ再利用だとすぐに気付くことができるかどうか......

まあ、こればっかりは、実際にある程度の規模のプロジェクトをやってみないとわかりませんね。