インターフェースによるポリモーフィズム
CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 41/107。
ポリモーフィズムは、共通のインターフェースを通じて、異なる型を統一的に扱うことを可能にします。Goでは、継承やクラス階層を使用せず、完全にインターフェースのみによってこれが実現されます。
関数がインターフェース型を引数として受け取る場合、そのインターフェースを実装する任意の具象型を渡すことができます。関数は具体的な型を知る必要はなく、インターフェースによって定義された振る舞いのみを必要とします:
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string { return "Woof!" }
type Cat struct{ Name string }
func (c Cat) Speak() string { return "Meow!" }
func MakeSound(s Speaker) {
fmt.Println(s.Speak())
}
これで、MakeSound は Speak() メソッドを持つ任意の型で動作するようになります:
func main() {
dog := Dog{Name: "Rex"}
cat := Cat{Name: "Whiskers"}
MakeSound(dog) // ワンワン!
MakeSound(cat) // ニャー!
}
同じ関数呼び出しでも、渡される実際の型に応じて異なる動作をします。これがポリモーフィズムの仕組みです。MakeSound 関数は一度記述するだけで、Speaker インターフェースを満たしている限り、無制限の数の型で動作します。
このアプローチにより、コードの柔軟性と拡張性が維持されます。「話す」機能を持つ新しい型を追加する場合でも、既存の関数を変更する必要はありません。インターフェースを実装するだけで、自動的に動作します。
チャレンジ
簡単ポリモーフィズムの動作を実証する車両記述システムを構築しましょう。インターフェースを通じて共通の動作を共有する異なる車両タイプを作成し、どの車両でも動作する単一の関数を記述します。
コードは2つのファイルに分けて構成します:
vehicles.go:Describe() stringメソッドを必要とするDescriberインターフェースを定義します。次に、このインターフェースを独自の方法で実装する3つの車両タイプを作成します:BrandとModelフィールドを持つCar— そのDescribe()は"Car: [Brand] [Model]"を返します。BrandとEngineCC(int) フィールドを持つMotorcycle— そのDescribe()は"Motorcycle: [Brand] [EngineCC]cc"を返します。Typeフィールド("Mountain" や "Road" など)を持つBicycle— そのDescribe()は"Bicycle: [Type]"を返します。
main.go: 任意のDescriberを受け取り、Describe()を呼び出した結果を出力するPrintDescriptionという関数を作成します。入力から車両の詳細を読み取り、各車両タイプのインスタンスを1つずつ作成し、それぞれをPrintDescriptionに渡して、同じ関数が3つの異なるタイプすべてで動作することを確認します。
以下の入力が提供されます:
- 1行目: 車のブランド (Car brand)
- 2行目: 車のモデル (Car model)
- 3行目: オートバイのブランド (Motorcycle brand)
- 4行目: オートバイのエンジン排気量 (Motorcycle engine CC、整数)
- 5行目: 自転車のタイプ (Bicycle type)
例えば、Toyota、Camry、Honda、600、Mountain が与えられた場合、出力は以下のようになります:
Car: Toyota Camry
Motorcycle: Honda 600cc
Bicycle: MountainPrintDescription が、受け取っているのが Car なのか、Motorcycle なのか、Bicycle なのかを知る必要がないことに注目してください。単に Describe() を呼び出すだけで、各タイプが独自の出力を返します。これがポリモーフィズムです。1つの関数で、複数の振る舞いを実現します。
チートシート
ポリモーフィズム(多態性)により、共通のインターフェースを通じて異なる型を統一的に扱うことができます。Goでは、継承やクラス階層を使用せずに、インターフェースを通じてこれを実現します。
関数がパラメータとしてインターフェース型を受け取る場合、そのインターフェースを実装している任意の具象型を渡すことができます:
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string { return "Woof!" }
type Cat struct{ Name string }
func (c Cat) Speak() string { return "Meow!" }
func MakeSound(s Speaker) {
fmt.Println(s.Speak())
}
同じ関数が異なる型で動作します:
func main() {
dog := Dog{Name: "Rex"}
cat := Cat{Name: "Whiskers"}
MakeSound(dog) // Woof!
MakeSound(cat) // Meow!
}
関数は一度記述すれば、インターフェースを満たしている限り、無制限の型で動作します。新しい型を追加しても、既存の関数を変更する必要はありません。ただインターフェースを実装するだけです。
自分で試してみよう
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
// TODO: 任意の Describer を受け取る PrintDescription という名前の関数を作成します
// そして Describe() を呼び出した結果を出力します
func main() {
reader := bufio.NewReader(os.Stdin)
// 車の詳細を読み込む
carBrand, _ := reader.ReadString('\n')
carBrand = strings.TrimSpace(carBrand)
carModel, _ := reader.ReadString('\n')
carModel = strings.TrimSpace(carModel)
// オートバイの詳細を読み込む
motoBrand, _ := reader.ReadString('\n')
motoBrand = strings.TrimSpace(motoBrand)
motoEngineStr, _ := reader.ReadString('\n')
motoEngineStr = strings.TrimSpace(motoEngineStr)
motoEngine, _ := strconv.Atoi(motoEngineStr)
// 自転車の詳細を読み込む
bicycleType, _ := reader.ReadString('\n')
bicycleType = strings.TrimSpace(bicycleType)
// TODO: 入力値を使用して Car、Motorcycle、Bicycle を作成します
// TODO: ポリモーフィズムを実演するために、各車両に対して PrintDescription を呼び出します
fmt.Println("TODO: Print vehicle descriptions")
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー