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()などをするときは構造体やポインタなどの実態を返す
  • 構造体が使われる時になってからインターフェースを定義する

サンプル

以上を踏まえた上で、擬似コードを書いて感覚を掴んでいきます。今回のサンプルでは、以下の図にあるDatabaseMainInterfaceの3つを実装してみます。(APIはインタフェースによる共通化の例なのでコードでは紹介しません)

f:id:y-zumi:20190728023343j:plain

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は軽量でとても使い勝手が良いのでちゃんと使っていきたいですね

参考文献

github.com

medium.com

qiita.com