Goにおけるダックタイピング
CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 42/107。
ダックタイピング(duck typing)という用語は、「もしそれがアヒルのように歩き、アヒルのように鳴くなら、それはアヒルである」という格言に由来します。Goにおいて、これは、ある型がインターフェースを実装していることを明示的に宣言する必要はなく、単に適切なメソッドを持っていればよいということを意味します。
次のインターフェースと、全く無関係な2つの型を考えてみましょう:
type Quacker interface {
Quack() string
}
type Duck struct{}
func (d Duck) Quack() string { return "Quack!" }
type Robot struct{}
func (r Robot) Quack() string { return "Beep-quack!" }
Duck も Robot も、その定義のどこにも Quacker について言及していません。それにもかかわらず、両方とも正しいシグネチャを持つ Quack() メソッドを持っているというだけで、このインターフェースを満たしています。
func MakeItQuack(q Quacker) {
fmt.Println(q.Quack())
}
func main() {
MakeItQuack(Duck{}) // クワッ!
MakeItQuack(Robot{}) // ビープ・クワッ!
}
この暗黙的な充足は、型が既に存在した後にインターフェースを定義できるため、非常に強力です。外部パッケージの型に一致するインターフェースを、そのソースコードを修正することなく作成できます。型はあなたのインターフェースについて知る必要はなく、ただ正しく振る舞うだけでよいのです。
Goにおけるダックタイピングは、コンパイル時の型安全性を維持しながら、動的言語の柔軟性を提供します。コンパイラは、型がインターフェース値として使用される前に、実際に必要なメソッドを実装しているかどうかを検証します。
チャレンジ
簡単ダックタイピングの動作を実証するメッセージングシステムを構築しましょう。共通のインターフェースを明示的に実装することを宣言せずに、共通のインターフェースを通じてメッセージを送信できる、完全に無関係な型を作成します。
コードは3つのファイルに分けて構成します:
messengers.go: たまたま同じ振る舞いを共有する、3つの無関係な構造体を作成します:Numberフィールドを持つPhone— そのSendMessage(text string) stringメソッドは"SMS to [Number]: [text]"を返します。Emailフィールドを持つComputer— そのSendMessage(text string) stringメソッドは"Email to [Email]: [text]"を返します。Nameフィールドを持つPigeon— そのSendMessage(text string) stringメソッドは"[Name] carries: [text]"を返します。
sender.go:SendMessage(text string) stringメソッドを必要とするMessengerインターフェースを定義します。Messengerのスライスとメッセージ文字列を受け取り、各メッセンジャーでSendMessageを呼び出した結果を含む文字列のスライスを返すBroadcastという関数を作成します。main.go: 入力から詳細を読み取り、各メッセンジャー型のインスタンスを1つずつ作成し、それらをMessengerのスライスにまとめ、Broadcastを使用してすべてのメッセンジャーを通じてメッセージを送信します。各結果をそれぞれの行に出力します。
以下の入力が提供されます:
- 1行目: 電話番号
- 2行目: メールアドレス
- 3行目: 伝書鳩の名前
- 4行目: 一斉送信するメッセージ
例えば、"555-1234"、"alice@mail.com"、"Winston"、そして "Hello World" が与えられた場合、出力は以下のようになります:
SMS to 555-1234: Hello World
Email to alice@mail.com: Hello World
Winston carries: Hello Worldここでの重要な洞察は、Phone、Computer、Pigeon には共通点がないということです。これらは基本型を共有しておらず、Messenger を明示的に実装しているわけでもありません。しかし、それらがすべて同じように「鳴く」(一致するメソッドを持っている)ため、Goはそれらすべてを Messenger 値として使用することを許可します。これがダックタイピングの仕組みです。
チートシート
Goにおいて、ダックタイピングとは、型がインターフェースを実装していることを明示的に宣言する必要がないことを意味します。単に、一致するシグネチャを持つ必要なメソッドを持っているだけで十分です。
インターフェースが必要とするすべてのメソッドを型が持っていれば、その型は暗黙的にインターフェースを満たします:
type Quacker interface {
Quack() string
}
type Duck struct{}
func (d Duck) Quack() string { return "Quack!" }
type Robot struct{}
func (r Robot) Quack() string { return "Beep-quack!" }
DuckとRobotの両方は、定義の中で言及することなくQuackerインターフェースを満たしています:
func MakeItQuack(q Quacker) {
fmt.Println(q.Quack())
}
func main() {
MakeItQuack(Duck{}) // Quack!
MakeItQuack(Robot{}) // Beep-quack!
}
この暗黙的な充足により、型が既に存在した後にインターフェースを定義したり、外部パッケージの型に一致するインターフェースをソースコードを修正することなく作成したりすることが可能になります。コンパイラは、型が必要なメソッドを持っていることをコンパイル時に検証し、柔軟性を維持しながら型安全性を提供します。
自分で試してみよう
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// 電話番号を読み込む
scanner.Scan()
phoneNumber := scanner.Text()
// メールアドレスを読み込む
scanner.Scan()
email := scanner.Text()
// 鳩の名前を読み込む
scanner.Scan()
pigeonName := scanner.Text()
// 配信するメッセージを読み込む
scanner.Scan()
message := scanner.Text()
// TODO: 各メッセンジャータイプ(Phone, Computer, Pigeon)を1つずつ作成する
// TODO: すべてのメッセンジャーをMessengerのスライスにまとめる
// TODO: Broadcastを使用して、すべてのメッセンジャーを通じてメッセージを送信する
// TODO: 各結果をそれぞれの行に出力する
// 変数を使用するためのプレースホルダー(実装時に削除してください)
_ = phoneNumber
_ = email
_ = pigeonName
_ = message
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー