Go: interfaceをunexportedにしてみる
まずはunexportedで
Goでは振る舞いを抽象化する際にインターフェイスを使用します。
また、インターフェイス分離の原則(ISP)等を守り、疎結合なコードを書くにあたっては、呼び出し側で呼び出し側が求めるようなインターフェイスを定義し、使用しないメソッドへの依存をさせないようにすると思います。
もしそのようにコードを書くのであれば、Goでは型は暗黙的にインターフェイスを満たすため、最初からそのインターフェイスをexportする必要はないのではないでしょうか。
先ほどの通り、Goでは型はインターフェイスのメソッドを実装するときに、そのインターフェイスを満たします(たとえばimplement
等で明示する必要がない)。つまり、たとえインターフェイスがunexportedであったとしても、そのインターフェイスがunexportedなメソッドを持たない限り、外部パッケージの型はそのインターフェイスを満たすことができます。
package main
// ...
func greetTo(w io.Writer, g greeter) {
fmt.Fprintln(w, g.Greet())
}
type greeter interface {
Greet() string
}
package l10n
type JA struct{}
func (JA) Greet() string {
return "こんにちは"
}
type EN struct{}
func (EN) Greet() string {
return "Hello"
}
l10n
パッケージのJA
やEN
は、main
パッケージのgreeter
インターフェイスを満たすので、main
パッケージのgreetTo
関数の第2引数に渡すことができます。
package main
import (
"errors"
"flag"
"fmt"
"io"
"os"
"github.com/tomocy/go-if/l10n"
)
var greeters = map[string]greeter{
"ja": l10n.JA{}, // l10n.JAはgreeterインターフェイスを満たす
"en": l10n.EN{}, // l10n.ENはgreeterインターフェイスを満たす
}
func main() {
if err := run(os.Stdout, os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(w io.Writer, args []string) error {
if len(args) < 1 {
return errors.New("too few arguments")
}
flags := flag.NewFlagSet(args[0], flag.ContinueOnError)
lang := flags.String("lang", "ja", "language to greet")
if err := flags.Parse(os.Args[1:]); err != nil {
return err
}
g, ok := greeters[*lang]
if !ok {
return fmt.Errorf("unsupported language: %s", *lang)
}
greetTo(w, g)
return nil
}
func greetTo(w io.Writer, g greeter) {
fmt.Fprintln(w, g.Greet())
}
type greeter interface {
Greet() string
}
確かに、io.Writer
のように、多くのパッケージで(引数としてや型のフィールドの型として)使用されることを想定する場合は、インターフェイスをexportすることはあると思います。
func main() {
if err := run(os.Stdout, os.Args); err != nil {
fmt.Fprintln(os.Stderr)
os.Exit(1)
}
}
func run(w io.Writer, args []string) error {
// ...
}
上記のようなコードはまさしくio.Writer
のおかげで、毎回io.Writer
のようなインターフェイスを定義する必要もなくなり、各パッケージが調和しています。
しかし、そのような場合(そのインターフェイスがとても一般的な振る舞いを表す)は実は少なく、多くの場合では、ISPを守りながら、まずはunexportedで定義しても良いのではないでしょうか。この場合、多少重複することもあると思いますが、その部分のために依存を増やすよりは結合度を低くすることの方がより重要ではないかと思います。
A little copying is better than a little dependency.
Go Proverbs - Rob Pike - Gopherfest - November 18, 2015
またインターフェイスにメソッドを追加することには後方互換性がないことからも、インターフェイスをexportするのは、多くのパッケージでそれが使われることと、その使用方法とが分かってからでも遅くはないのではないでしょうか。
doNotImplement
ちなみに上記で触れたように、インターフェイスのexported、unexportedに関わらず、unexportedなメソッドをインターフェイスに追加することで、外部のパッケージの型が満たすことのできないインターフェイスを作ることができます。
package main
// ...
type greeter interface {
doNotImplement
Greet() string
}
type doNotImplement interface {
doNotImplement()
}
package l10n
// ...
type JA struct{}
func (JA) Greet() string {
return "こんにちは"
}
func (JA) doNotImplement() {}
先ほどの例のmain
パッケージのgreeter
インターフェイスにunexportedなメソッドを追加すると、たとえl10n
パッケージのJA
やEN
がdoNotImplement
メソッドをもったとしても、どちらもgreeter
インターフェイスを満たさなくなるので、以下のようにコンパイルエラーになります。
./main.go:14:2: cannot use l10n.JA literal (type l10n.JA) as type greeter in map value:
l10n.JA does not implement greeter (missing doNotImplement method)
have l10n.doNotImplement()
want doNotImplement()
./main.go:15:2: cannot use l10n.EN literal (type l10n.EN) as type greeter in map value:
l10n.EN does not implement greeter (missing doNotImplement method)
have l10n.doNotImplement()
want doNotImplement()
Discussion