今さら訊けない? Go言語のInterfaceについてその本質をズバリ解説。

2021年1月11日GolangGo

今回はわかったようでモヤモヤする、Go言語のインターフェースについて解説します。これを読めばあなたもGo言語のインターフェースについてその本質を理解できるはず!

Go言語のInterfaceを解説するにあたって

今回はその本質を簡潔に解説することを目的としています。そのため細かい所や議論の余地のある所はすっ飛ばします。今後の進め方として、この記事の内容を基礎として、他のサイトや本でさらに周辺の知識を深めていただければと思います。

まずは、その本質を理解してモヤモヤを消しましょう!

また、筆者はU.S在住のため、説明文中、日本語訳した用語が適切でない場合があります。その場合は英語を正としてください。

キーワードは"Polymorphism"と"Implicit"

Go言語のInterfaceを説明する上で重要なキーワードは"Polymorphism“と"Implicit“. 「ポリモーフィズム」が「暗黙的」に備わる。と覚えておいてください。その内容は順を追って説明します。

Interfaceがなぜ必要か?

まずはInterfaceがない場合を簡単な例を使って説明します。

以下のようにmonkey human という、簡単な構造体structが定義されているとします。

<monkey struct> 要素はNameだけの簡単なStructであり、2つのメソッドimitate()makeADream()を持っています。

type monkey struct { Name string }
func (m monkey) imitate(){
  fmt.Println("Imitating")
}
func (m monkey) makeADream(){
  fmt.Println("Make a Dream!")
}

<human struct> 要素はNameだけの簡単なStructであり、2つのメソッドtalk()makeADream()を持っています。

type human struct { Name string }
func (h human) talk(){
  fmt.Println("Talking")
}
func (h human) makeADream(){
  fmt.Println("Make a Dream!")
}

メイン関数内でこれらStructsが利用され、変数taroとjackが定義されています。試しにそれぞれ特有のメソッドであるimitate()とtalk()を実行しています。

func main() {
  taro := monkey{Name:"TARO"}
  jack := human{Name:"JACK"}

  taro.imitate()
  jack.talk()
}

結果は以下のようになります。当たり前ですが。

> go run main.go
Imitating
Talking


ここからが本題です。月日は流れ、プログラムに次のようなgoToTheMoon()という処理を足す必要が出てきました。

func main() {
  taro := monkey{Name:"TARO"}
  jack := human{Name:"JACK"}
          :<省略>
  //今回追加される関数。
  goToTheMoon(<引数は?>)
}

ここで、「1. 月に行けるのはhumanだけ!」という仕様の場合、このgoToTheMoon()の定義は以下となり、引数にはhuman typeの変数が入ります。

1. humanのみ

func main() {
  taro := monkey{Name:"TARO"}
  jack := human{Name:"JACK"}
           :<省略>
  goToTheMoon(jack)   //human typeのJackが引数となる
}

func goToTheMoon(h human){  //関数定義で引数はhuman typeと指定。
  fmt.Println("Go to the moon")
}

この時、もし以下のようにmonkey typeである taroを引数に入れてしまうと当たり前ですがコンパイルエラーが出ます。

goToTheMoon(taro)  //monkey typeのtaroを入れるとコンパイルエラー


ここでプロジェクトの仕様が変更されたとします。やっぱり、「human だけではなくmonkeyも月に行く」ことになりました。そのため、「2. monkey用のgoToTheMoon()」を追加する必要がでてきました。

2. humanだけではなくmonkey用のgoToTheMoon()を追加

func main() {
  taro := monkey{Name:"TARO"}
  jack := human{Name:"JACK"}
        :<省略>
  goToTheMoon1(jack)
  goToTheMoon2(taro)
}

func goToTheMoon1(h human){   //human用goToTheMoon
  fmt.Println("Go to the moon")
}
func goToTheMoon2(m monkey){  //monkey用goToTheMoon
  fmt.Println("Go to the moon")
}

名前の衝突を避けるため、goToTheMoon1(),goToTheMoon2()としました。

問題:

上のコードを見ても同じような記述が多く出現して、もう既に嫌な感じですよね。

さらにここで、human, monkeyだけではなく、条件を満たした他のtypeも月に行く処理が必要となった場合、非常に困ります。cat type用、dog type用とgoToTheMoon関数を増やしていくのでしょうか?


Interfaceのポリモーフィズムを利用する

先ほどの問題をInterfaceを使って解決します。

goToTheMoon()関数を一つにまとめます。そして、条件を満たした変数ら全てがこの関数を使用できるようにします。順を追って実装しますね。

Step1. 月に行く関数(goToTheMoon)を以下のように定義して、引数を仮にastronaut typeとします。

func goToTheMoon(a astronaut){
  fmt.Println("Go to the moon")
}

ここで、「え、"astronaut“タイプって何?」と思ったかもしれませんが、心配いりません。ここでは仮にそう置いただけです。次のステップからこのastronaut typeを定義していきます。

Step2. 月に行く条件を決める。(goToTheMoon関数の引数となれる条件を決める。)

goToTheMoon()関数の引数であるastronaut を Interfaceを使って定義します。ここがGo言語のInterfaceの真骨頂です。

まずはどう定義するか書かせてください。説明はそのあとにします。

<Interface定義>

type astronaut interface {
  makeADream()
}

この定義ですが、以下のように読みます。(超大事)

makeADream()メソッドを持っているものはみんなastronaut interfaceだよ。

つまり、monkey humanは最初に定義した通り、makeADream()メソッドを既に持っています。そのため、これらのtypeで作成された変数 taro jackastronaut typeにもなれるわけです。上のインターフェースを定義することでこのポリモーフィズムが暗黙的にもたらされたことになります。

<monkey と humanは 最初からmakeDreamメソッドを持っていたため、、、>

<変数 taro, jackどちらもgoToTheMoonの引数になることができる>

func main() {
  taro := monkey{Name:"TARO"}
  jack := human{Name:"JACK"}

  goToTheMoon(jack)  //jackはhuman typeだけではなくastronaut typeにもなりえるのでエラーにならない。
  goToTheMoon(taro)  //taroはmonkey typeだけではなくastronaut typeにもなりえるのでエラーにならない。
}

func goToTheMoon(a astronaut){
  fmt.Println( "Go to the moon")
}


インターフェースとは契約である。※1

極端に書いていますが間違いではないです。上の例では元のmonkeyやhuman typeの定義には一切、変更を加えていません。

しかしながら、インターフェース定義で

「makeADream()メソッドを持っているものは全てastronautタイプにもしてあげる。」

といういわゆる契約をしたわけです。

この契約のおかげでこれまでmonkey typeとして作成されていたtaro, human typeとして作成されていたjackもastronaut type となることができます。その結果、goToTheMoon()関数が使えるようになりました。もちろんこれらの変数以外にもmakeADream()メソッドを持つ変数があればそれらも全て暗黙的にastronaut typeとしてふるまうことができます。

上の契約(インターフェース定義)のおかげて、条件を満たす変数たちに「astronaut typeにもなれるよ」というポリモーフィズムが自動的に広まったことがわかると思います。

余談ですがここでは自動的にと書きましたが、明示的に各変数やタイプの宣言や定義を変えていないのにこのように使えるようになるため「暗黙的(Implicit)に」という言い回しがよく使われます。

※1.詳しく知りたい方は名著「The Go Programming Language (Alan A.A. Donovan) 」"Interfaces as Contracts"をお勧めします。


何処で役に立つ?もっと知りたい。

これまでの説明を読んでこれたのであれば、もうInterfaceの本質は理解できたはずです。そしてその上で疑問が生まれたはず。そう、この暗黙的なポリモーフィズムの広がりを理解したが故に使いたい。どこで使えばよい?

次回は使い方について例をご紹介しようと思います。(このネタが好評であればですが。。。)

GolangGo

Posted by Kanata