2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Go言語のinterfaceとダックタイピングのお話 (オブジェクト指向)

Last updated at Posted at 2022-12-10

初めに

Go言語はclassという概念が存在しないため他のオブジェクト指向の言語と比較すると書き方に違いがある場合が多々あります

 そこで今回は主にinterfaceとダックタイピングに焦点を絞って説明したいと思います

ダックタイピングとは

以下wikipediaからの引用文になります

オブジェクトがあるインタフェースのすべてのメソッドを持っているならば、たとえそのクラスがそのインタフェースを宣言的に実装していなくとも、オブジェクトはそのインタフェースを実行時に実装しているとみなせる、ということである。それはまた、同じインタフェースを実装するオブジェクト同士が、それぞれがどのような継承階層を持っているのかということと無関係に、相互に交換可能であるという意味でもある。

"If it walks like a duck and quacks like a duck, it must be a duck"
(もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない)

何をいってるんだ?こいつは、となっていると思いますので順を追って説明していきたいと思います

その前にinterfaceとは

ダックタイピングを理解するためにはinterfaceの理解が必要なので少し面倒かもしれませんがお付き合いください
(すでに理解している方は「本題のダックタイピング」まで飛ばしてください)

interface(インターフェイス)は、クラスがどのようなメソッドを持っているのかをあらかじめ定義する、いわば設計書のような存在です

具体的な処理はinterface内には記述せずに継承(実装)するタイミングで書いていきます

Go言語におけるinterfaceの振る舞い

例えば以下はjavaでのinterfaceの書き方になります


interface Sports {
    public abstract void Dribble();
    public abstract boolean Pass(Str x);
    public abstract int Shoot(int y);
    // ...

}

 interfaceはあくまで設計書なので具体的なことは書きません

 ここからわかることといえば、型と引数とメソッド名から何がやりたいのかがなんとなく想像がつくくらいなものです。

あー、ドリブルしてパスしてシュートするっていうメソッドがあるのねぇ、へー

みたいな感じです

正直ここはGo言語でも同じです

type Sports interface {
    Dribble()
    Pass(x string) bool
    Shoot(y int)
}

じゃあどこが違うのか

それは実装するinterfaceを明示しているかどうかです

例えばjavaだと

class Soccer implements Sports{
    public boolean Pass(Str x){
        //成功したかどうかの処理
        // ...
    };
    public int Shoot(int y){
        //何点入ったかの処理
        //サッカーは1点だがバスケなどは2,3点
    }
    // ...
}

と言うようにどのinterfaceを実装したのかがしっかりと明示されている(implements Sportsってね)

 しかしGoは構造体にinterfaceと同じメソッドを全て追加することでinterface型が使えるようになります

具体例

 よくわからないと思うのでここでは具体的なコードを書いて説明したいと思います

と言うわけでGo言語で実装開始

 今回はドリブル,パス,シュートをしてパスは選手によって成功と失敗が起こるようにしています

main.go
type Sports interface {
	Dribble()
	Pass(x string) bool
	Goal(y int)
}
type Soccer struct {
	Player string
}

func (s *Soccer) Dribble() {
	fmt.Println("ボールを蹴って相手ゴールに向かっていきます!")
}

func (s *Soccer) Pass(x string) bool {
	if x == "デブライネ" {
		fmt.Println("パス成功!...ボールが足に吸い寄せられていきます...")
		return true
	} else {

		fmt.Println("パス失敗...足元の技術が足りない...!")
		return false
	}
}

func (s *Soccer) Goal(y int) {
	if s.Pass(s.Player) {
		fmt.Println("1点追加です!")
	}
}

最初のinterfaceはjavaと同じです

ただここからが違います

interfaceを実装するにあたってjavaであれば

class Soccer implements Sports

とやっていたところをGo言語ではinterfaceを見ながら同じ種類のメソッドを1つずつ追加しています

 実はGo言語は同じメソッドを構造体に実装すれば、自動的にインターフェースが実装されているということになっているのです

 だからjava見たくわざわざこのinterfaceを実装したんだよ〜って明示する必要はありません

 そしてその後はsoccerのインスタンスを生成,そしてSports型のsportという名前の変数を実装し、それをsportに代入

さらにそれをOffenceに渡すという処理をしています

ちなみにここでメソッドが一致しないとエラーを吐いてくれます

出力結果は以下になります。

main.go
soccer := Soccer{"デブライネ"}
var sport Sports
sport = &soccer
fmt.Println("~~~~~~~サッカーの試合~~~~~~~")
Offence(sport, 1, soccer.Player)
shell.
~~~~~~~サッカーの試合~~~~~~~
ボールを蹴って相手ゴールに向かっていきます!
パス成功!...ボールが足に吸い寄せられていきます...
1点追加です!

  • interfaceとそれを実装した構造体があったとき,構造体はinterfaceの型を用いて宣言できます
    • 人間に例えると,サルから進化した人間もサルという型を使っても良いといったところでしょうか

メリット

 以上のように実装方法はわかったければいまいちメリットを感じられないと思います

なのでここからはinterfaceを用いると何がいいのかを紹介していきます

interfaceのいいところその1

構造体が別の型でも同じような処理は使い回すことができる

 上の処理ではサッカーをメインとして扱ってきました。

 しかしドリブル、パス、シュートをするスポーツは他にもあります、そうバスケットボールとかね

 そういった時に一連の処理は同じだけど扱う構造体によって振る舞いを変えたいとします

以下ではバスケットボールのメソッドをinterfaceを用いた場合の処理です

またサッカーの時とは構造体の形が少し違います

main.go
type BasketBall struct {
	Player string
    PlayerNum int
}

func (b *BasketBall) Dribble() {
	fmt.Println("ボールを地面について相手のゴールに向かっていきます")
}

func (b *BasketBall) Pass(x string) bool {
	if x == "ストックトン" {
		fmt.Println("パス成功!...変幻自在のパス回しです!")
		return true
	} else {

		fmt.Println("パス失敗...あっと手からボールが離れてしまった...!")
		return false
	}
}

func (b *BasketBall) Shoot(y int) {
	if b.Pass(b.Player) {
		if y == 2 {
			fmt.Println("ダンクシュート!!")
		} else if y == 3 {
			fmt.Println("3ポイントシュートです!!")
		}
	}

}
func main() {
	basketball := BasketBall{"カリー", 5}
	var sport Sports
	fmt.Println("~~~~~~~バスケの試合~~~~~~~")
	sport = &basketball
	Offence(sport, 2, basketball.Player)
}
shell.
~~~~~~~バスケの試合~~~~~~~
ボールを地面について相手のゴールに向かっていきます
パス失敗...あっと手からボールが離れてしまった...!

 バスケはサッカーとは違いボールを蹴るわけではないですし、シュートの種類によって2点なのか3点なのかと点数も違います

ただ、スポーツとしての振る舞いはおなじです

パスしてシュートを打ってゴールする、ほら?一緒でしょ?

こういった時に活躍するのがinterfaceとなっています

interfaceのいいところその2

振る舞いは後で決めればいい

例えばあなたが迷路を作るとします、一番効率の良い作り方はどういった作り方になるでしょうか?

これは入り口と出口を先に定めてしまうと言うことです

 入り口と出口さえ決まっていればあとはそれに合わせて内側を作っていけばいいだけですからね

これはシステムを開発する上でも同じことです

 メソッドの具体的な中身は後で実装していくことにして、引数と戻り値だけを先に決めてしまいます

 具体的な処理は後で考えてまずは処理の種類を先に決めておく、そうすることで開発が滞ることなく進めることができますし、処理の変更を反映させやすいです

interfaceのいいところその3

役割ごとにディレクトリを分けることができる(関心の分離ができる)

 例えばRDBからNoSQLに変えるとしましょう

この時sql文をまるっきし変えなくてはいけないんですが、実際やりたいCRUD操作って一緒なんですよね

 こんな時、処理がいろんなところに散らばって書いてあったらどうでしょうか?めちゃくちゃ描き直すのが面倒じゃないですか?

 しかし、やりたい処理を表してるけど、具体的な実装はしてない層(interfaceで実装)とその層を基に具体的な処理をしている層に分けていたら

具体的な処理をしている層のみを書き換えればいいことになるんですよ

つまり、根本的なロジックは変わらないのでinterfaceをそのまま用いて、具体的なロジックのSQLだけを変えればスムーズにリファクタリングができるってわけですよ

本題のダックタイピング

やっと今回の本題であるダックタイピングという言葉が出てきました

ここまでこれば、wikipediaに書いてあった

オブジェクトがあるインタフェースのすべてのメソッドを持っているならば、たとえそのクラスがそのインタフェースを宣言的に実装していなくとも、オブジェクトはそのインタフェースを実行時に実装しているとみなせる

という言葉の意味が理解できるようになったと思います。つまりGo言語のように宣言的に実装しない言語における仕様であるということですね

具体例

ここからは具体例で見ていきましょう

ここでは野球の大谷選手が二刀流であるということを例にとって実装していきます

main.go
type Pitcher interface {
	ThrowBall()
}

type Batter interface {
	HitBall()
}

func Strike(p Pitcher) {
	p.ThrowBall()
	fmt.Println("ストライク!!")
}
func Homerun(b Batter) {
	b.HitBall()
	fmt.Println("ホームラン!!!")
}

type Otani struct {
}

func (o Otani) ThrowBall() {
	fmt.Println("ボールを投げます")
}
func (o Otani) HitBall() {
	fmt.Println("ボールを打ちます")
}

func main() {
	otani := Otani{}
	Strike(otani)
	Homerun(otani)

}
shell.
ボールを投げます
ストライク!!
ボールを打ちます
ホームラン!!!

解説

type Pitcher interface {
	ThrowBall()
}

type Batter interface {
	HitBall()
}
func Strike(p Pitcher) {
	p.ThrowBall()
	fmt.Println("ストライク!!")
}
func Homerun(b Batter) {
	b.HitBall()
	fmt.Println("ホームラン")
}
  1. まずはピッチャーとバッターという2つのインターフェースを作成しております

  2. 次にそれぞれのinterfaceを引数にとる関数を実装しています

    • この時、違うinterfaceを引数にとっているなぁってことが分かれば十分です
type Otani struct {
}

func (o Otani) ThrowBall() {
	fmt.Println("ボールを投げます")
}
func (o Otani) HitBall() {
	fmt.Println("ボールを打ちます")
}

次に大谷選手の構造体を実装しております

そしてその後実装した大谷選手の構造体にピッチャーとバッターのinterfaceを実装(継承)しています

func main() {
	otani := Otani{}
	Strike(otani)
	Homerun(otani)

}

そうすることで大谷選手の構造体は2つのinterfaceを実装したので(宣言的ではない)
大谷選手の構造体はPitcher型とBatter型で宣言ができるようになったことになります

つまりPitcher型とBatter型の2つの型を持っているということです!

そのためPitcher型を引数にとるStrike関数とBatter型を引数にとるHomerun関数の引数にも大谷選手の構造体を使えるようになったんですねぇ

これは大谷選手がピッチャーとバッターの振る舞いをすることができるため、もうそれはピッチャーであり、バッターであるということができる。そのためピッチャーとバッターを引数にとる関数に使ってもいいよということなのです

"If it walks like a duck and quacks like a duck, it must be a duck"
(もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない)

もし大谷選手がボールを投げ,ホームランを打つのであれば、そればピッチャーでありバッターであるに違いない
ってことですね〜

これがダックタイピングになります

まとめ

Go言語は宣言的にinterfaceの実装を用いないためダックタイピングという書き方を使うことができます

本来ピッチャーしか受け取らないストライク関数とバッターしか受け取らないホームラン関数を大谷さんの構造体を用いるとどちらの引数にも使うことができるようになりました

これは大谷さんがピッチャーとバッターのどちらの振る舞いもメソッドとして持っていたからということなのですね

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?