01.3 埋め込みと合成 — 継承の代わりに「部品を組み込む」¶
多くのオブジェクト指向言語には継承(inheritance)があります。親クラスを子クラスが引き継ぎ、 振る舞いを再利用したり上書きしたりします。
Go には extends のような継承がありません。その代わりに埋め込み(embedding)という仕組みで
「型の中に他の型を部品として組み込む」合成(composition)を行います。
このレッスンのゴール:
- 構造体への埋め込み(匿名フィールド)を書ける
- メソッド昇格(promotion)を理解する
- 埋め込みとメソッドの上書きで「継承っぽい」ことを Go 流に実現できる
1. 継承の何が問題だったか¶
「動物」という親クラスを作り、「犬」「猫」がそれを継承する、というのはよくある継承の例です。
// 他言語の継承のイメージ
class Animal { name; speak() }
class Dog extends Animal { speak() { return "ワン" } }
class Cat extends Animal { speak() { return "ニャー" } }
継承は便利ですが、深い継承ツリー(Animal → Mammal → Dog → ...)を作ると 「どの層のどのメソッドが呼ばれるのか」が分からなくなり、変更が全体に波及します。
Go の流儀は「継承する」のではなく「部品として組み込む」ことです。
2. 埋め込みの基本¶
構造体に型名だけを書くと、その型が埋め込まれます(匿名フィールド)。
type Dog struct {
*Animal // 型名だけ: 埋め込み
}
埋め込まれた型のメソッドは、外側の型のメソッドとして昇格(promotion)します。
つまり dog.Name() と書けば、内部の Animal.Name() が呼ばれます。
import (
"errors"
"fmt"
"strings"
"github.com/janpfeifer/gonb/gonbui"
)
// ErrUnanswered は、練習問題が未回答のときにプレースホルダ関数が返す特別なエラー。
var ErrUnanswered = errors.New("未回答: この関数はまだ実装されていません")
// UnansweredStr は string を返すプレースホルダが未回答のときのセンチネル。
const UnansweredStr = "【未回答】"
type Animal struct {
name string
}
func (a *Animal) Name() string {
return a.name
}
func (a *Animal) Speak() string {
return "……(動物の鳴き声)"
}
// Dog は *Animal を埋め込む。Animal のフィールドとメソッドを引き継ぐ。
type Dog struct {
*Animal
}
// Dog 独自の Speak を定義すると、Animal.Speak を「上書き」できる。
func (d *Dog) Speak() string {
return "ワン!"
}
type Cat struct {
*Animal
}
func (c *Cat) Speak() string {
return "ニャー"
}
ここがポイント¶
DogはAnimalを埋め込んでいるので、dog.Name()が昇格して呼べるDog.Speak()を独自に定義することで、Animal.Speak()を上書きする(ポリモーフィズムの準備)- 継承ツリーでなく「Animal という部品を持つ」という関係になる
3. 実際に動かしてみる¶
犬と猫を作って、メソッドがどう解決されるかを見ます。
var dog = &Dog{Animal: &Animal{name: "ポチ"}}
var cat = &Cat{Animal: &Animal{name: "タマ"}}
%%
fmt.Printf("dog.Name() = %s (Animal から昇格したメソッド)\n", dog.Name())
fmt.Printf("dog.Speak() = %s (Dog が上書き)\n", dog.Speak())
fmt.Printf("cat.Name() = %s\n", cat.Name())
fmt.Printf("cat.Speak() = %s (Cat が上書き)\n", cat.Speak())
dog.Name() = ポチ (Animal から昇格したメソッド) dog.Speak() = ワン! (Dog が上書き) cat.Name() = タマ cat.Speak() = ニャー (Cat が上書き)
メソッドの解決関係を表で見える化します。
func methodTable() string {
var b strings.Builder
b.WriteString(`<table border="1" cellpadding="4" style="border-collapse:collapse">
<tr><th>呼び出し</th><th>結果</th><th>定義元</th></tr>`)
b.WriteString(fmt.Sprintf(`<tr><td>dog.Name()</td><td>%s</td><td>Animal(昇格)</td></tr>`, dog.Name()))
b.WriteString(fmt.Sprintf(`<tr><td>dog.Speak()</td><td>%s</td><td>Dog(上書き)</td></tr>`, dog.Speak()))
b.WriteString(fmt.Sprintf(`<tr><td>cat.Name()</td><td>%s</td><td>Animal(昇格)</td></tr>`, cat.Name()))
b.WriteString(fmt.Sprintf(`<tr><td>cat.Speak()</td><td>%s</td><td>Cat(上書き)</td></tr>`, cat.Speak()))
b.WriteString(`</table>`)
return b.String()
}
%%
gonbui.DisplayHTML(methodTable())
gonbui.Sync()
| 呼び出し | 結果 | 定義元 |
|---|---|---|
| dog.Name() | ポチ | Animal(昇格) |
| dog.Speak() | ワン! | Dog(上書き) |
| cat.Name() | タマ | Animal(昇格) |
| cat.Speak() | ニャー | Cat(上書き) |
読んでください: Name() はどの型でも Animal の定義が使われ、Speak() はそれぞれの型が
自分の定義で上書きしています。この「共通のものは部品から、独自のものは自分で」が合成の基本です。
4. 直感・類推: レゴブロック¶
継承は「親の型をそっくり受け継いで、一部を書き換える」イメージです。 埋め込み(合成)は「部品を組み合わせて、必要なものを足す」イメージです。
- 動物という「部品」(名前を持つ、という機能)を、犬にも猫にも組み込む
- 鳴き声(Speak)はそれぞれの型が独自に持つ
レゴで車を作る時、「車」という親クラスを継承するのではなく、 タイヤ・車体・エンジンという部品を組み合わせるのと同じ発想です。 バックエンドでは「ログ出力」「認証」「DB接続」などの横断的な機能を、継承でなく 部品(埋め込み・interface)として組み込む設計が推奨されます。
練習問題 1.3: Bird を追加しよう¶
Animal を埋め込んだ Bird 型を作ってください。
仕様:
type Bird struct {
*Animal
}
func (b *Bird) Speak() string // "ピヨ" を返す
func (b *Bird) Fly() string // "飛んだ" を返す
*Animalを埋め込む(Name()が昇格する)Speak()は"ピヨ"を返すFly()は"飛んだ"を返す(Bird 独自のメソッド)
// YOUR CODE HERE
// type Bird struct { *Animal } と Speak/Fly を実装してください。
// (未実装のままチェックセルを実行すると「未回答」と表示されます)
type Bird struct {
*Animal
}
func (b *Bird) Speak() string {
return UnansweredStr
}
func (b *Bird) Fly() string {
return UnansweredStr
}
チェックのためのヘルパー¶
import "reflect"
import "fmt"
func mustEqual(got, want any, name string) {
if reflect.DeepEqual(got, want) {
fmt.Printf("✅ Passed: %s\n", name)
return
}
panic(fmt.Sprintf("❌ %s\n got = %v (%T)\n want = %v (%T)", name, got, got, want, want))
}
%%
bird := &Bird{Animal: &Animal{name: "小太郎"}}
if bird.Speak() == UnansweredStr || bird.Fly() == UnansweredStr {
fmt.Println("⚠️ 未回答: 練習問題を解いてから、このセルを再度実行してください")
} else {
mustEqual(bird.Name(), "小太郎", "Name() が Animal から昇格")
mustEqual(bird.Speak(), "ピヨ", "Speak() が Bird の定義")
mustEqual(bird.Fly(), "飛んだ", "Fly() が Bird 独自")
fmt.Println("🎉 すべてのチェックが通りました")
}
⚠️ 未回答: 練習問題を解いてから、このセルを再度実行してください
まとめ¶
- Go に継承は無い。代わりに埋め込み(型名だけのフィールド)で部品を組み込む
- 埋め込んだ型のメソッドは昇格して外側の型のメソッドとして呼べる
- 外側の型で同じ名前のメソッドを定義すれば上書きになる
- 継承ツリーでなく「部品を持つ」合成で設計する
答え合わせは 01.3-embedding-composition-solutions.ipynb で行ってください。
次は 01.4 で「抽象化とポリモーフィズム(interface)」を学びます。