【Go】インターフェースの意義と使い方

前回に引き続きGoのアウトプット。
前回の記事はこちら。

https://www.sunapro.com/go-pointer/

今回はインターフェースについて。

目次

インターフェースって何?

メソッドの型だけを記述した型のことで、インターフェースを通してオブジェクトの振る舞いを定義することが可能。

以下のように記述する。

type <型名> interface {
  メソッド名(引数の型, ...) (返り値の型, ...)
  ...
}

インターフェースによって特定の型が「どのようなメソッドを実装すべきか」を規定することができる

なぜインターフェースを使うのか?

目的の一つは処理の共通化なのだが、言葉だけでもよく分からないのでどういう場合に使うのかを見ていく。

まずは前提条件から。
ここでは三角形の面積と正方形の面積をそれぞれ計算して、それを出力する関数を定義してみる。

// 構造体の定義
type triangle struct {
	height float64
	base   float64
}

type square struct {
	sideLength float64
}

// 面積を計算する関数の定義
func (t triangle) getArea() float64 {
	return 0.5 * t.base * t.height
}

func (s square) getArea() float64 {
	return s.sideLength * s.sideLength
}

// 出力する関数の定義
func printTriangleArea(t triangle) {
	fmt.Println(t.getArea())
}

func printSquareArea(s square) {
  fmt.Println(s.getArea())
}

func main() {
	t := triangle{height: 3, base: 6}
	s := square{sideLength: 5}

	printTriangleArea(t)
	printSquareArea(s)
}

全体的な構成は以下の通りになっている

① 三角形と正方形の構造体の定義

② それぞれの面積値を計算する関数の定義

③ それぞれの面積値を出力する関数の定義

上の3つに対して三角形と正方形でそれぞれ2つずつ定義されている。

もしここに長方形の面積や台形の面積を計算する関数を加えたかったら① ~ ③がまたそれぞれ必要になってしまう。

したがって、共通化できる処理の部分は共通化したい。
どこが共通化できそうか?

①の構造体の定義や②の面積値の計算はそれぞれの図形に特有のものなので共通化は厳しそう。
③の計算値を出力する関数というのは図形の形に限らず共通のロジックなので共通化できそうである。

しかしここで問題が生じる。
面積値を出力する関数は以下のように引数を取るが、この際に型を規定する必要がある

func printArea(引数 <引数の型>) {
	fmt.Println(引数.getArea())
}

そのため、型が違う三角形や正方形のたびに関数を別途用意しなければならなくなる。

実際に先ほどの例では以下のように別々に用意していた。

// 出力する関数の定義
func printTriangleArea(t triangle) {
	fmt.Println(t.getArea())
}

func printSquareArea(s square) {
  fmt.Println(s.getArea())
}

ここで引数の型をtriangleやsquareに縛られないより汎用的な形で渡すことができれば共通化できる。

それを実現できるのがインターフェースである。

インターフェースを使った実装

では実際に実装していく。

本来あるべき実装の手順とは違うかもしれないが、今回は処理の流れやどういうときにインターフェースを使いたくなるかを考慮した手順で実装していく。

まず先ほどの例で出力関数で受け取る引数を汎用的にしたいので以下のようにする。

func printArea(sh shape) {
  fmt.Println(sh.getArea())
}

引数に受け取る形をshape型として、図形の構造体をより汎用的に表すものにした。
そしてその引数をレシーバーにしてgetArea関数を呼び出している。

当然shape型はまだ定義していないので以下のように定義する。

type shape interface {
  getArea() float64
}

これはshapeという型は必ずgetAreaメソッドを持っていることを保証している。

実装としてはたったこれだけでprintTriangleAreaとprintSquareAreaの2つの関数をprintAreaという関数にまとめることができた。

最終的な全コードは以下の通り。

package main

import "fmt"

type shape interface {
	getArea() float64
}

type triangle struct {
	height float64
	base   float64
}

type square struct {
	sideLength float64
}

func printArea(sh shape) {
	fmt.Println(sh.getArea())
}

func (t triangle) getArea() float64 {
	return 0.5 * t.base * t.height
}

func (s square) getArea() float64 {
	return s.sideLength * s.sideLength
}

func main() {
	t := triangle{height: 3, base: 6}
	s := square{sideLength: 5}

	printArea(t)
	printArea(s)
}

インターフェースの実装はいつやった?

ここで一つ疑問。

先ほどの例で、printAreaの引数にはインターフェースを指定していたが、関数を呼び出す時点ではただの構造体を渡しているだけでインターフェースを渡すという情報はどこにも含まれていないように見える。

func printArea(sh shape) { //インターフェースを引数にとっている
	fmt.Println(sh.getArea())
}

.
.
.

func main() {
	t := triangle{height: 3, base: 6} //構造体
	s := square{sideLength: 5} //構造体

	printArea(t) //構造体を引数に渡している
	printArea(s) //構造体を引数に渡している
}

なんとGoでは、インターフェースの中にあるメソッドと同一のメソッドが全て実装されている構造体には自動的にインターフェースが実装されるらしい。

つまりここでは(t triangle) getArea()(s square) getArea()がそれぞれ実装されているためtriangleとsquareの構造体は自動的にインターフェースとしてみなされるということらしい。

逆に言えば、ある構造体をインターフェースとして機能させるためには、インターフェースの中の関数を全て実装する必要があるということになる。

インターフェースのこの意味はTypeScriptでも同じような感じだったな
ただしTypeScriptではimplementsを使って明示的にインターフェースを実装する必要があった。

あわせて読みたい
TypeScript インターフェース 前回に引き続きTypeScriptについてのアウトプット。今回はインターフェースについて。 ちなみに前回はTypeScriptの型の基本について書いた。 https://www.sunapro.com/t...

このあたりGoは記述を簡単にする工夫が凝らされているなーと感じる。

まとめ

インターフェースを使うことで型に柔軟性を持たせることができる。
これによって型の差を吸収してくれて汎用的な処理を実装することができるようになる。

また、インターフェースを定義することによって、その構造体が実装する関数を強制的に定義することができるようになる。

おわりに

最初難しく感じたインターフェースだったけど中々よくまとめられたと思う。
スッキリした。

今回みたいな簡単な例ではなく業務での実装みたいにコードが複雑になってくると真価を発揮する気がする。

処理を共通化したいと思ったときにインターフェースが使えないかどうか意識してみる。

参考

Goのinterfaceがわからない人へ
https://qiita.com/rtok/items/46eadbf7b0b7a1b0eb08

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次