🍵

Go: interfaceをunexportedにしてみる

2020/10/01に公開

まずは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パッケージのJAENは、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
https://www.youtube.com/watch?v=PAAkCSZUG1c&t=9m28s

またインターフェイスにメソッドを追加することには後方互換性がないことからも、インターフェイスを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パッケージのJAENdoNotImplementメソッドをもったとしても、どちらも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