LoginSignup
193
130

More than 5 years have passed since last update.

Go言語のInterfaceの考え方、Accept interfaces,return structs

Last updated at Posted at 2018-07-20

Go言語のInterface

Go言語の優れた特徴の一つとして、Interfaceが挙げられることがあります。
Interfaceを持つJavaやC#のような他言語と比べ、GoのInterfaceの言語機能における主な違いは
静的言語のように静的にチェックされるメソッドの羅列を宣言できるinterfaceでありながら
ダックタイピングや型アサーションによって動的言語のように柔軟な使い方ができることです。

しかしこれだけでは単に、ケースに合わせて使い分けることができる選択肢を得ただけのように聞こえます。
実際にはGo言語のパッケージ、型システム、そして文化的特徴によってGo言語特有の軽量でシンプル、そして有機的なInterfaceを表現できます。
Go特有でGoらしいGoのInterfaceです。

Accept interfaces, return structs

GoのInterfaceのこの特性について、Jack lindamood氏の
"Accept interfaces, return structs"というイディオムが比較的浸透していると思います。
インターフェースを受け入れて構造体を返しなさい、
もう少し具体的に言うとパッケージでは処理する側でinterfaceを定義し引数として受け取り、New関数の戻り値など外部にわたすものはstructのように具体的な型にすべきとのことです。

これは伝わりやすくするためにやや誇大で、実際にはInterfaceの使い方の一つであり絶対的な指標ではありません。
Go言語にも実装する側が定義するInterfaceがあります。
hash.Hashなどがそうです。多様な暗号化形式のそれを仮想化したHashのInterfaceです。

しかし上記の文は間違いなくGo言語のInterfaceの特徴をよく表しています。

比較的公式に近い記述としてはgo wikiのコードレビューコメントページにあるInterfaceの項目
APIの実装側でInterfaceを定義すべきでないとの記述があります。
2017年初めごろに追加されたようなので比較的最近のようですね。
JSON-RPCの記事も実際のリファクタリングで行った話で参考になります。

サンプルコード

ありがちな動物の例でいうと

animal.go
    //こういった概念的なinterfaceを宣言すべきでない
    //type Animal interface{
    //  Speak()
    //  Run()
    //}
    type Dog struct{}
    func (Dog) Speak(){fmt.Println("bowwow")}
    func (Dog) Run(){fmt.Println("yay,I'm running")}

    type Cat struct{}
    func (Cat) Speak(){fmt.Println("mewmew")}
    func (Cat) Run(){fmt.Println("shut up ****er")}
main.go
    //処理に必要なものだけを宣言する
    type Runner interface{
        Run()
    }
    func main(){
        Race([]Runner{animal.Dog{}, animal.Cat{}})
    }

    func Race(runners []Runner){
        for _,runner :=range runners{
            runner.Run()
        }
    }


上記のように、実装する側でなく処理を行う側でInterfaceを定義し、構造体を直接使います。

これによって例えばロボでもレースに参加することができますし
話相手がほしいのであればSpeakerとしても宣言し扱うことができます。
また、走れないイルカさんのような例外も許容できます。
でもキュイキュイ言えますからね。

このイディオムは、必要な機能のみを宣言することで動物以外の走れる存在に対する拡張性、
すでにあるインターフェースと競合することなく独立したSpeakerを宣言できる直交性、
その他依存の少なさやインターフェースの小ささ、正確さなどに繋がります。
上記の例はいささか抽象的すぎるのでもう少し具体的な例でいきます。

もしある構造体をファイルに保存する関数を非常に実直に作るとすれば

convert.go
    func Save(f *os.File,doc *Document) error

しかしos.Fileしか受け取らないため、テストも難しく実ファイルを要求します。
もちろんinterfaceによってこれを解決するわけですが
仮にos.Fileがinterfaceだとしてもまだ問題があります。

os.go
package os
type File interface{
    Read([]byte) (int,error)
    Write([]byte) (int,error)
    Seek(int64,int) (int64,error)
    Close()error
}
//Save はDocumentの内容をfに書き込む
func Save(f os.File,doc *Document) error

今回のSaveでは書き込み(Write関数)しか必要ないとすると
上記はSOLID原則のインターフェース分離の原則を満たしていません。
それによってstringのようなReadしかできない型、bytesのように読み書きしかできない型と互換性がなく
os.Fileの機能すべてが必要な場合は問題ありませんがbytes.Buffer型を代替にしたテストを行うような柔軟性に欠けます。
他の言語であれば通常、単にインターフェースを分けますがこれにも問題があります。

(ちなみにos.FileをinterfaceにすべきというProposalがRob pike氏から実際に提案されてはいます。
go2の話なので特に結論は出ていませんが色々問題があり、個人的にBcmills氏の意見に賛成です。)

os.go
package os
type File interface{
    Reader
    Writer
    Seeker
    Closer
}
func Save(f os.Writer,doc *Document) error

今回のSaveのようなWriterやReader単体ごとの機能のみ必要な場合は問題ありませんが
ダックタイピングのない継承形式の言語ではReadWriterやReadWriteSeekCloserなどの組み合わせとなってくると継承が一気に複雑になります。
ReadWriterとSeekWriterとReadCloserと...といったような継承は見たくないですね。
os.Readerとか突っ込みたいところはあるかもしれませんが
少なくともinterfaceを継承する他言語では問題があることはわかってもらえたと思います。
しかしGo言語でもまだ問題があります。

これは実際に使われることになるioパッケージでも同じことです。
ReadWriter,SeekWriter...どう使われるかわからない実装側がインターフェースを定義することには無理も無駄もあり
かといってすべて網羅すればあまりにも醜いです。
ioパッケージでもReadWriteSeeker,ReadWriteCloserはありますがReadWriteSeekCloserはありません。

Accept interfaces

ioのような汎用インターフェースパッケージであればあまりによく使われるインターフェースはパッケージ側が使わずとも定義する価値はありますが
Go言語では、必要であれば必要な側が定義すべきです。
仮にosにインターフェースがあればインターフェース以外のosの実装との依存関係が発生しますし(かつてerrorがos.Errorだった時と同じ)
必要とした側が定義すれば自然に最小限かつパッケージが名前空間となることも合わさってインターフェースの意味がより明示的になりやすいです。

ファイルを保存するには書き込む機能だけが欲しいので

convert.go

    type Writer interface{
        Write([]byte) (int,error)
    }

    func Save(w Writer,doc *Document) error

もちろん本当はioパッケージがWriterを持っているのでWriterでなくio.Writerを使うことになるでしょう。
これではinterfaceを必要とする側で定義していないのではないかと思われるかもしれませんが
ioパッケージはファイルの実装でも文字列の実装でもありません、汎用的な入出力処理そのものに対するツールセットであり
Readerを引数にしてReaderを返す類のラップ関数を除いて、引数で受け取る関数ばかりでReaderを生成するようなことはありません。
実際にReadを実装する型を返すのはstringsやbytes,osなどの具体的な内容を持つパッケージです。

重要なのは必要なインターフェースのみに依存出来ることです。その関数を満たしていればそのインターフェースが自分のものであってもサードパーティのものであっても構いません。
提供しないことではなく要求できることに価値があります。

詳細(実装)は抽象(インターフェース)に依存すべきとの言葉がありますが
JavaやC#のインターフェースに依存するとき、それはある意味では同時にインターフェースの出処にも依存していました。
Go言語ではユーザーが定義したインターフェースを標準ライブラリが満たすことも可能です。

Goにおいて他パッケージのインターフェースを使う場合、その多くはioのようなユーティリティの範囲か、そのパッケージがそのInterfaceを必要としてるかがほとんどです。
他言語では本当に単純で普遍的なインターフェースでしかインターフェース分離の原則を綺麗に守ることは難しいものでした。
Goではそれが容易です。
また処理する側が必要なものを定義するだけなので、メソッドは少なく、より正確で、そこに依存関係そして階層はありません。

Return structs

return structsの部分はもう少しシンプルです。
インターフェースが最小限で正確なAPIでなければいけないなら処理を簡略化するためのユーティリティ関数は定義すべきでありません。
例えばJavaのListインターフェースにはaddとaddAllがありますがaddがあればfor文でaddを回せばaddAllと同等のことが出来るので効率の事情があってもユーティリティ関数の類です。
そういった本質的でない関数はインターフェースには追加すべきでなく、構造体の関数として持ったり、または関数化することで対応するべきです。
よって多くのケースでは構造体を返したほうが理にかなっています。
例えばbytes.Readerには各Read関数だけでなく初期化やサイズ情報などの関数を持っています。

オブジェクト指向ではカプセル化とよく聞く言葉ですが
Goのインターフェースは実装を隠すのでなく実装に依存しないというべきかもしれません。

ダックタイピングであるための問題点

Go言語のInterfaceにおけるダックタイピングについて
実装を強制できないという話がありますが、

var _ Interface = (*Impl)(nil)

これでできます。

それにGoの言語仕様自体は全くではありませんが誤ったコードを咎めるようなことはしません。
appendなどの組み込み関数は容易くオーバーライド出来るので勝手にappendを変な関数にされてしまうと問題ですが
そういったプログラマが非常に稀であることを考慮し、言語のシンプルさを天秤にかけると
それらは教育やツールの役割として扱った方がメリットが大きいと考えています。
上記のインターフェースの実装の保証も、プロジェクトレベルの話で標準ライブラリでは使用されません。
インターフェースにおいてもダックタイピングが生んだメリットはコストを上回るものだと思います。

次に、結局関数のシグネチャが一致しないといけない以上実装を知っているようなもので意味がなく、実際にはインターフェースを意識しないといけないことについてですが
これは確かにそのとおりで似たようなことをやっていてもEqualがEqだったり,fmtではPrintfなのにtesting.TではLogfだったりします。
標準にあるレベルであればLoggingライブラリのように大体サードパーティも合わせてくれますが保証はされません。
より実践的なレベルではある程度ラップして使うケースも多いです。

それでもソースレベルでの依存が無いこと、そしてなにより型階層のない構成指向のGoの気風を巧みに表現していること、表現できることには価値があります。

Goのインターフェース

Goのインターフェースは型の境界です。

Goにとってインターフェースは汎用的とは限りません。
ReaderやWriterのような汎用的なものはしばしばioパッケージのようにユーティリティを集約したパッケージとして提供されますが
Comparatorでなくsort.Interface
Serializableでなくjson.Unmarshaler,gob.Unmarshalerといったように
それぞれのパッケージが必要なものをインターフェースとして定義しています。
またGo Playgroundなどのもう少し実践的なソースではLoggerをインターフェースにするなどしています。Go言語らしいロギングについて

Goのインターフェース、いいですね。

参考

Proposal:os.FileをInterfaceにすべき
Jack lindamood氏のAccept interfaces記事
Dave cheney氏のSOLID GO Design
JSON-RPC: a tale of interface

その他Accept interfaces関連記事
https://blog.chewxy.com/2018/03/18/golang-interfaces/
http://idiomaticgo.com/post/best-practice/accept-interfaces-return-structs/

193
130
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
193
130