メソッドの昇格
CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 36/107。
構造体の埋め込みがフィールドを外部の構造体にプロモート(昇格)させるのと同様に、Goは埋め込まれた型からのメソッドもプロモートします。これは、埋め込まれた構造体で定義されたメソッドを、外部の構造体から直接呼び出せることを意味します。
type Engine struct {
Horsepower int
}
func (e Engine) Start() string {
return "Engine started"
}
type Car struct {
Model string
Engine // 埋め込み
}
Car は Engine を埋め込んでいるため、Start() メソッドは自動的に昇格されます:
func main() {
c := Car{
Model: "Sedan",
Engine: Engine{Horsepower: 200},
}
fmt.Println(c.Start()) // エンジンが始動しました
fmt.Println(c.Engine.Start()) // こちらも動作します
}
この昇格(promotion)は、インターフェースにおいて重要な意味を持ちます。埋め込まれた型がインターフェースを満たしている場合、外側の型も自動的にそのインターフェースを満たすことになります。
type Starter interface {
Start() string
}
func Ignite(s Starter) {
fmt.Println(s.Start())
}
func main() {
c := Car{Model: "Sedan", Engine: Engine{Horsepower: 200}}
Ignite(c) // CarはEngineを通じてStarterを満たします
}
Car 型は Starter を明示的に実装することはありませんが、埋め込まれた Engine が必要なメソッドを持っているため、インターフェースを満たしています。これが、Goが伝統的な継承を使わずに振る舞いの再利用を実現する方法です。
チャレンジ
簡単埋め込み型のメソッドが外部型にどのようにプロモート(昇格)されるか、そしてこれがコンポジションを通じてどのようにインターフェースの充足を可能にするかを示す音楽プレイヤーシステムを構築しましょう。
コードは2つのファイルに分けて構成します:
audio.go: 音楽プレイヤーの構成要素を作成します:Brandフィールド(string)を持ち、[Brand] playing audioを返すPlay() stringメソッドを持つAudioPlayer構造体Play() stringメソッドを要求するPlayableインターフェースAudioPlayerを埋め込んだ、Modelフィールドを持つSmartphone構造体AudioPlayerを埋め込んだ、Modelフィールドを持つTablet構造体
main.go: 任意のPlayableを受け取り、Play()を呼び出した結果を返すStartPlaybackという関数を作成します。入力からデバイス情報を読み取り、Smartphone と Tablet の両方を作成し、どちらの型も明示的にPlay()を実装していないにもかかわらず、埋め込まれたAudioPlayerを通じてPlayableインターフェースを満たしていることを示します。
以下の入力が提供されます:
- 1行目: Smartphone のモデル名
- 2行目: Smartphone のオーディオブランド
- 3行目: Tablet のモデル名
- 4行目: Tablet のオーディオブランド
各デバイスについて、そのモデル名を出力し、続いてそのデバイスを StartPlayback に渡した結果を出力してください。
例えば、iPhone 15、Apple Audio、iPad Pro、Beats が与えられた場合、出力は以下のようになります:
iPhone 15
Apple Audio playing audio
iPad Pro
Beats playing audioAudioPlayer の Play() メソッドが自動的に各外部型にプロモートされるため、追加のコードなしで Playable インターフェースを満たし、Smartphone と Tablet の両方を StartPlayback に渡すことができる点に注目してください。
チートシート
構造体が別の型を埋め込むと、埋め込まれた型のメソッドは自動的に外側の構造体に昇格されます:
type Engine struct {
Horsepower int
}
func (e Engine) Start() string {
return "Engine started"
}
type Car struct {
Model string
Engine // 埋め込み
}
func main() {
c := Car{
Model: "Sedan",
Engine: Engine{Horsepower: 200},
}
fmt.Println(c.Start()) // Engine started
fmt.Println(c.Engine.Start()) // こちらも動作します
}
埋め込まれた型がインターフェースを満たしている場合、外側の型も自動的にそのインターフェースを満たします:
type Starter interface {
Start() string
}
func Ignite(s Starter) {
fmt.Println(s.Start())
}
func main() {
c := Car{Model: "Sedan", Engine: Engine{Horsepower: 200}}
Ignite(c) // CarはEngineを通じてStarterを満たします
}
これにより、従来の継承を使用せずに、コンポジションを通じて振る舞いの再利用が可能になります。
自分で試してみよう
package main
import (
"bufio"
"fmt"
"os"
)
// TODO: 任意の Playable を受け取り、
// Play() を呼び出した結果を返す StartPlayback 関数を作成してください
func main() {
scanner := bufio.NewScanner(os.Stdin)
// スマートフォンのモデルを読み込む
scanner.Scan()
smartphoneModel := scanner.Text()
// スマートフォンのオーディオブランドを読み込む
scanner.Scan()
smartphoneBrand := scanner.Text()
// タブレットのモデルを読み込む
scanner.Scan()
tabletModel := scanner.Text()
// タブレットのオーディオブランドを読み込む
scanner.Scan()
tabletBrand := scanner.Text()
// TODO: AudioPlayer を埋め込んだ Smartphone を作成してください
// TODO: AudioPlayer を埋め込んだ Tablet を作成してください
// TODO: スマートフォンのモデルを出力し、その後スマートフォンを引数にして StartPlayback を呼び出してください
// TODO: タブレットのモデルを出力し、その後タブレットを引数にして StartPlayback を呼び出してください
_ = smartphoneModel
_ = smartphoneBrand
_ = tabletModel
_ = tabletBrand
fmt.Println("TODO: Complete the implementation")
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー