😀

Go言語でのインターフェースを使った実装と継承について

2024/03/09に公開

概要

Golangでのインターフェースを用いた実装と継承についてまとめる

それぞれ簡単にまとめると
実装は、オブジェクトの果たすべき役割をインターフェースで決めてそれを守らせる事
継承は、あるオブジェクトと共通の役割を持つオブジェクトには、それを丸々引き継がせる事

インターフェース とは

オブジェクトが持つメソッド(役割)を宣言するための雛形のこと
インターフェースから派生したオブジェクトはこの宣言されたメソッドを持つ

以下のコードでは、IHumanというインターフェースを作成している

type IHuman interface {
  GetName()
}

実装 とは

インターフェースからオブジェクトを派生させることである
Golangでは実装は、インターフェースで定義した関数と同じ名前のメソッドを持つ構造体を作ることで達成できる

以下のコードでは、上のコードで定義したインターフェースIHumanのGetNameという関数を、構造体Humanがメソッドとして定義している
これで、HumanはIHumanを実装したことになる

下のコードのmain関数では、Humanオブジェクトを格納する変数kenの型にIHumanというインターフェースを用いて宣言している
変数の宣言にインターフェースIHumanを用いる事で、HumanがIHumanを実装できていなければ警告が出る
また、IHumanを使って宣言する事で、nameプロパティを直接触ることが出来ずカプセル化される

package main

import "fmt"

type IHuman interface {
	GetName() string
}

type Human struct {
	name string
}

func (h Human) GetName() string {
	return h.name
}

func main() {
	// IHuman型で宣言することで
	// HumanがIHumanを実装できていなければエラーになる
	var ken IHuman = Human{
		name: "ken",
	}

	fmt.Println(ken.GetName())
}

##継承 とは
既存のオブジェクトのプロパティとメソッドを全て、新しく定義するオブジェクトに引き継がせること
Golangでは継承は、埋め込みという方法で行う

以下のコードでは、Citizenという構造体を定義しており、その時にHuman構造体を定義内容に埋め込んでいる
CitizenがHumanの役割を持ったまま、languageの情報を持つ役割を付与している

Citizen型がHuman型を継承することで、Citizen型のtaroがHuman型のGetNameメソッドも呼べることが以下のコードから分かる
当然Citizen型で定義したGetLangも呼べる

package main

import "fmt"

type IHuman interface {
	GetName() string
}

type Human struct {
	name string
}

func (h Human) GetName() string {
	return h.name
}

type Citizen struct {
	// Humanを埋め込む
	Human
	language string
}

func (c Citizen) GetLang() string {
	return c.language
}

func main() {
	var taro Citizen = Citizen{
		Human: Human{
			name: "taro",
		},
		language: "Japanese",
	}

	fmt.Println(taro.GetName())
	fmt.Println(taro.GetLang())
}


##上のコードの問題点
main関数内で、変数taroを定義する時にインターフェースではなく構造体Citizenを用いて宣言している
それはICitizenというインターフェースを定義していないからである
これでは開発者が意図したようにCitizen型を定義できているか確認できない
そこで以下のようにICitizenというインターフェースを定義して、main関数では変数taroの定義時にICitizenを用いる

package main

import "fmt"

type IHuman interface {
	GetName() string
}

type Human struct {
	name string
}

func (h Human) GetName() string {
	return h.name
}

type ICitizen interface {
    // IHumanを埋め込む
	IHuman
	GetLang() string
}

type Citizen struct {
  // Humanを埋め込む
	Human
	language string
}

func (c Citizen) GetLang() string {
	return c.language
}

func main() {
	// ICitizen型で宣言することで
	// CitizenがICitizenを実装できていないとエラーになる
	var taro ICitizen = Citizen{
		Human: Human{
			name: "taro",
		},
		language: "Japanese",
	}

	// ICitizenはgetNameとgetLangを持つ
	fmt.Println(taro.GetName())
	fmt.Println(taro.GetLang())
}


##関数の引数はインターフェースにする
後で例も見るがその理由は、Human型とCitizen型で共通の内容を処理させたい時に、わざわざ二つも関数を用意したくないから
IHumanを引数に持つ関数では、Human型でもHuman型を継承したCitizen型でも受け取ることができて関数一つで同じ処理をさせる事ができる
もし、関数の引数がHumanやCitizenと言う構造体で宣言されていたら、その関数はHumanもしくはCitizenしか渡せず、一つの関数で済むのに二つ用意する羽目になる
同じ役割を持つから継承させたのに、わざわざ異なる関数を用意するのは無駄

例を見る

以下のコードではIHuman型を引数に持つtreatEqually(h IHuman)と言う関数を定義している
その関数はHuman型とCitizen型両方を受け取る事ができる

もう一つ定義したdiscriminate(c ICitizen)関数はICitizenを実装しているCitizen型しか受け付けない
Human型はIHumanは実装しているがICitizenは実装していないから受け入れられない
実際にHuman型を渡すとエラーが発生する

package main

import (
	"fmt"
)

type IHuman interface {
	GetName() string
}

type Human struct {
	name string
}

func (h Human) GetName() string {
	return h.name
}

type ICitizen interface {
	IHuman
	GetLang() string
}

type Citizen struct {
	Human
	language string
}

func (c Citizen) GetLang() string {
	return c.language
}

// Humanを受け入れる CitizenもOK
func treatEqually(h IHuman) string {
	return h.GetName()
}

// Citizenだけ受け入れる HumanはNG
func discriminate(c ICitizen) {
         return c.GetLang()
}

func main() {
	var ken Human = Human{
		name: "ken",
	}
	var taro Citizen = Citizen{
		Human: Human{
			name: "taro",
		},
		language: "Japanese",
	}

	// どちらも受け入れる
	fmt.Println(treatEqually(ken))
	fmt.Println(treatEqually(taro))

	// kenはHumanだから受け入れられない
	discriminate(ken) // エラー
	discriminate(taro)
}

##二つの別種のインターフェース
継承関係にはない二つのインターフェースがあり、実装したいオブジェクトがその両方の役割を持つ場合は、その二つを埋め込んだインターフェースを新しく定義してやれば良い

下のコードでは、IHumanとIFishという二つのインターフェースを定義している
それを用いてMermaid(魚人)を定義したいとする
その時はIMermaidと言うインターフェースを以下のように定義する事で両方の役割を持ったインターフェースができる

package main

type IHuman interface {
	Hanakokyu()
}

type IFish interface {
	Erakokyu()
}

type IMermaid interface {
	IHuman
	IFish
}

type Mermaid struct {
}

func (m Mermaid) Hanakokyu() {

}

func (m Mermaid) Erakokyu() {

}

func main() {
	var mermaid IMermaid = Mermaid{}

	mermaid.Hanakokyu()
	mermaid.Erakokyu()
}


##まとめ
実装と継承の方法をまとめた
変数の定義や関数の引数にはインターフェースを用いる事で安全に無駄なく実装ができる

Discussion