NO IMAGE

【Go】結局interfaceとは何なのか?

NO IMAGE

今まで避けつづけてきた(コラコラ)単体テストをGo言語で実装することになり、あらためてinterfaceについて調べました。どうやらモックに関わってくるようなので。

一般的な説明

Goのインタフェースについて、『プログラミング言語Go』(アラン・ドノバン&ブライアン・カーニハン/丸善出版)ではどんな風に書いてあるか、見てみました。

インタフェース型は、他の型の振る舞いに関する一般化あるいは抽象化を表します。インターフェースは一つの特定の実装の詳細に結び付けられていないので、インタフェースを用いて一般化することでより柔軟で適応性のある関数を書くことができます。

(『プログラミング言語Go』P.197)

私はJavaから入ってきたクチなので、この説明はウンウンとうなづけます。乱暴に言えば、なんやかんや融通が利かせられるということですね。わかりにくい人は多態性(ポリモーフィズム)について覗いてみるのが良いかもしれません。

その一方、Goのインタフェースに特有の点もあるようです。

Goのインタフェースの特徴的なところは、インタフェースは暗黙的に満足されるということです。言い換えると、具象型が満足するすべてのインタフェースを宣言する必要はないということです。つまり、単に必要なメソッドを持っているだけで十分です。この設計により、既存の型を変更することなく既存の具象型が満足する新たなインタフェースを作ることができます。これは、自分で変更できないパッケージに定義されている型に対して特に有益です。

(上掲書、同ページ)

つまり、こちらの記事にもある通り、インタフェースは「メソッドの塊」であり、インタフェースが「期待するメソッドをすべて満たした変数には、自動的にInterfaceが実装」されるということですね。こうした言語の内部で複雑さを隠蔽するような仕様も、Simplicityという設計思想に則っているように思われます。

実際に活用してみる

とはいえ言葉では腑に落ちないところも大きいので、実際に使ってみましょう。今回は以下のようなディレクトリ構成を持っている簡単なプロジェクトを考えます。

hello
├src
|└ product
|  └ products.go
└main.go

ここで、products.goの実装は以下のようになっています。

package product

type Product interface {
	GetTitle() string
}

func FetchTitle(p Product) string {
	return p.GetTitle()
}

type Book struct {
	Id    int64
	Title string
}

func (b *Book) GetTitle() string {
	return "Book: " + b.Title
}

type Music struct {
	Title string
}

func (m *Music) GetTitle() string {
	return "Music: " + m.Title
}

初めにGetTitle()の抽象を持つインタフェースProductを定義した上で、構造体BookとMusicを実装しています。これらはいずれも、返り値にstring型の値を持つGetTitle()をそれぞれ独自に定義しているので、Goにおいては「Productインタフェースを実装している」と言えます。

ここでのポイントはProductインタフェース型の引数をとる関数FetchTitle()です。想定通り、しっかりとBook, MusicともにProductインタフェースが実装されているなら(Book、MusicがProductを満足するなら)、そのいずれの型の変数もFetchTitle()の引数としてとれる、ということになります。

では、このことを確かめてみましょう。main.goを以下のように実装します。

package main

import (
	"fmt"
	"hello/src/product"
)

func main() {
	goBook := &product.Book{
		Id:    1,
		Title: "hoge",
	}

	goMusic := &product.Music{
		Title: "fuga",
	}

	fmt.Printf("%s\\n", product.FetchTitle(goBook))
	fmt.Printf("%s\\n", product.FetchTitle(goMusic))
}

// 実行結果
// Book: hoge
// Music: fuga

うん、想定通りですね。*product.Book型のgoBook、*product.Music型のgoMusicのいずれもがProductインタフェース型の引数を取るproduct.FetchTitle関数の引数として機能していることがわかりました。

この仕様を利用して、テストのモック等も考えていけそうです。

参考