Goのおかげでinterfaceがわかった
はじめに
プログラミングのレベルがだんだん習熟してきたときに壁となって立ちはだかることでおなじみのinterface。
手続き的な処理を実装するのに使うわけではなく、あくまで設計的な観点で登場する概念ということもあり、また、似たような使われ方をする「抽象クラス」との混同から、interfaceを苦手に感じる方もいるかと思います。
ただ、自分がGo言語を学んで業務で使っていく中で、interfaceというもののコンセプトが腑に落ちたので、Goのinterfaceをベースに、interfaceの勘所を説明していきたいと思います。
interfaceの勘所
interfaceは「interfaceを設計として実装をする側(内部向け)」ではなく、「interfaceを利用する側(外部向け)」の視点で考えるとわかりやすいです。
ここで、「インターフェース」と名前がつくものを他に考えてみましょう。
たとえば「ユーザーインターフェース(UI)」を考えてみます。
あるアプリケーションを考えたときに、「送信」ボタンを押すことでフォームに入力された内容が送信されることや、「閉じる」ボタンを押したときにアプリケーションが閉じることをユーザーは知っています。
ただその一方で、どこに情報を送るか、閉じる際にどういった処理をしているかといったような、具体的に行っている内部処理をユーザーが知る必要はなく、ただ「入力した内容が送信されること」や「アプリケーションが閉じられる」ことだけを理解できれば十分なわけです。
そしてこれらは異なるアプリケーションでも、内部の具体的な処理は異なるにせよ、最終的に得られる結果の観点では基本的に同じです。
次にAPIについて考えます。APIはApplication Programming Interfaceの略で、れっきとしたインターフェースです。
APIは「こういう形式でリクエストを送れば、こういう形のデータが返ってくる」という約束事を提示して、ユーザーは内部で行われている具体的な処理を知らずとも、そのルールに沿っていれば、正常なレスポンスが受け取れることを理解できます。
ここで、プログラミング言語の中で登場するinterfaceを、上記の例をもとに捉えてみます。
つまり、interfaceは具体的な内部処理を示さず、利用者に「必要なインプット」と「処理によって得られる結果」に関する約束事を提示するための要素だと言うことができます。
そう考えると、よくごっちゃになりがちな抽象クラスとの違いが明確になります。
抽象クラス:クラスの共通処理を定義したり、継承先のクラスで記述しなければいけないメソッドを定義するもの。視点としては継承する側(内部向け)のもので、複数の継承先に対してメソッド実装の強制力を働かせることができる。
interface:利用者との約束事を定義するもの。視点としては利用者側(外部向け)のもので、具体的な中身を知らずとも、「どういうメソッドがあり、渡すデータと返ってくるデータの形式が何であるか」ということを知ることができる。
Goのinterface
ここでGo言語のinterfaceを見てみます。
Goのinterfaceでは「implements」を明示的に書くことはせず、interfaceで定義したメソッドが実装されていれば、その型は自動的に「interfaceを実装した」ことになる、とよく説明されます。
ただ自分としては、上記の捉え方よりも、interfaceを「満たす」と考えるほうがわかりやすいと思っています。
以下はGoのサンプルコードです。
Go特有の書き方はあるにせよ、おおまかな雰囲気は掴めるんじゃないかと思います。(業務的になにも意味ないコードで申し訳ないです、、)
package main
// interfaceを定義
type MyInterface interface {
Hoge(string) (string, error)
Fuga() error
}
// 構造体を定義
type MyStruct struct {
a string
b int32
}
// Mystructのポインタ型に対してhogeメソッドを定義
func (*MyStruct) Hoge(s string) (string, error) {
// 何らかの処理
return s, nil
}
// Mystructのポインタ型に対してfugaメソッドを定義
func (*MyStruct) Fuga() error {
// 何らかの処理
return nil
}
// この関数はMyInterface型を受け取る
// MyInterfaceを「満たす」(定義したメソッドを実装している)ものであればどんなものでも受け取り可能。
func HogeFuga(s MyInterface) error {
// 具体的な処理内容は知らないけれど、このメソッドにstring型の変数を渡して実行すれば(string, error)が返ってくることを知っている
hoge, err := s.Hoge("hoge")
if err != nil {
return err
}
err = s.Fuga()
if err != nil {
return err
}
// なんらかの処理
return nil
}
func main() {
// *MyStruct型の変数を作成する
ms := &MyStruct{a: "a", b: 42}
// 関数を実行する
// この関数に渡すms(*MyStruct型)は、MyInterfaceを「満たす」ので、関数に渡すことができる
err := HogeFuga(ms)
// なんらかの後続処理が行われる
}
goのinterfaceでは内部(implementsする側)について特に考慮はせず、単に「満たしているか」で判定します。
内部向けの明示的なimplementsを排除しているおかげで、前項での考え方を非常にシンプルに理解できるかと思います。
最後に
本記事で紹介した内容は、あくまで「interfaceを理解する上での勘所」をGoのinterfaceをベースに説明したものであって、interfaceはこれが全てではないです。
ただ、interfaceを理解するとっかかりとして、上記のような考え方、理解の仕方は十分に使えるものだと思います。
最後まで読んでいただきありがとうございました。