GoのInterfaceの作法 "Accept Interfaces, Return structs"
はじめに
Goにおける抽象化の方法にインターフェースがあります。インタフェースをうまく使うことにより具体的な実装を隠し、疎結合な実装ができるようになります。
これにより、テストがやりやすくなったり、リファクタリングの影響範囲を小さくできるなどのメリットがあります。インターフェースがないコードは振る舞いで共通化することができないため、冗長かつ密結合なシステムになってしまいます。
今回はGoのインターフェースの基本的な考え方である "Accept interfaces, Return structs" について紹介します。
"Accept interfaces, Return structs" とは
直訳で 「インターフェースを受け入れて、構造体を返す」 ですが、この考え方はJack Lindamood氏が過去に提案したものです。
GoのCodeReviewのドキュメントには以下のように書かれています。
Go Code Review Comments - Interfaces
Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values. The implementing package should return concrete (usually pointer or struct) types.
(中略)
Do not define interfaces before they are used: without a realistic example of usage, it is too difficult to see whether an interface is even necessary, let alone what methods it ought to contain.
訳してみると次のようになります。
「一般的にGoのInterfaceは、構造体などの値を実装するpackageではなく値を使用するpackageに属します。値を実装しているpackageは、通常はポインターや構造体型を返す必要があります。
構造体などの値が他のpackageから使用される前に実装に対するInterfaceを定義しないでください。まだ使われていないときに実装したInterfaceが必要であるかは判断が難しいためです。」
簡単に箇条書きで整理してみます。
- インターフェースは構造体を利用する側のパッケージに定義する
- 引数をインターフェースで抽象化して受け取る
NewXxxx()
などをするときは構造体やポインタなどの実態を返す- 構造体が使われる時になってからインターフェースを定義する
サンプル
以上を踏まえた上で、擬似コードを書いて感覚を掴んでいきます。今回のサンプルでは、以下の図にあるDatabase
とMain
とInterface
の3つを実装してみます。(API
はインタフェースによる共通化の例なのでコードでは紹介しません)
Database
ここではInterfaceを定義せず、Databaseの構造体と初期化処理、各種メソッドを定義しています。また、初期化処理のNewBookDBClient
では実態である構造体のポインタを返しています。
type BookDB struct {} func NewBookDBClient() *BookDB { return new(BookDB) } // write系 func (*d BookDB) Add(book Book) { ... } func (*d BookDB) Remove(id string) { ... } // read系 func (*d BookDB) GetByID(id string) *Book { ... } func (*d BookDB) GetByTitle(title string) *Book { ... }
Interface & Main
今回のmainパッケージで要求されるユースケースをIDから本を検索する
としましょう。この場合、InterfaceはGetByID(string) *Book
のみを定義し、インターフェース分離の原則に則って、必要のない関数には依存しないようにします。これにより、Write系とID検索以外のRead系のメソッドは、このパッケージからは利用できないようになります。
// read系のGetByIDのみが使える type BookFetcher interface { GetByID(string) *Book } func main() { fetcher := db.NewBookDBClient() bookID := "12345" book := SearchBook(fetcher, bookID) } // 引数をInterfaceにすることで、不必要な関数に依存しなくなる func SearchBook(fetcher BookFetcher, bookID string) *Book { return fetcher.GetByID(bookID) }
まとめ
今回のサンプルで分かった "Accept Interfaces, Return structs" の良い点は以下のようなものがあります。
- Interfaceで要求する関数が簡潔に定義されるため、
main
では必要のない関数について考えなくてもよくなる Database
のパッケージで新しい関数を追加しても、main
パッケージへの影響がない(今の実装の場合)
GoのInterfaceは軽量でとても使い勝手が良いのでちゃんと使っていきたいですね