🦛

Goで理解する「継承」と「インターフェース」の違い — 再利用と契約の設計思想

に公開

「継承(inheritance)」と「インターフェース(interface)」、どちらも共通化の仕組みですが、目的はまったく違います。
この記事では、Goのコードを例に「再利用」と「契約」の視点から整理し、
さらにクラシカルなオブジェクト指向言語との違いにも触れながら解説します。


🐤 継承とは — 実装の再利用

継承(スーパークラス)は、共通の実装を再利用する仕組みです。
Goでは「構造体の埋め込み」で似たことができます。

type Animal struct{}
func (Animal) Eat() { fmt.Println("Eating...") }

type Dog struct {
    Animal // ← 継承っぽい再利用
}

func main() {
    d := Dog{}
    d.Eat() // Animalの実装を再利用
}

🐰 Dogに新しいメソッドを足すのはOK(いくらでも追加してよい)
🐼 Eat() を Dog 側で上書きしたいなら、同じシグネチャ(引数・戻り値・レシーバ)で再定義すればDog側が優先される
(埋め込み元を明示したい場合は d.Animal.Eat() で呼べる)

type Animal struct{}
func (Animal) Eat() { fmt.Println("Animal eats") }

type Dog struct{ Animal }
func (Dog) Eat() { fmt.Println("Dog eats quickly") } // ← 上書き

func main() {
    d := Dog{}
    d.Eat()          // "Dog eats quickly"
    d.Animal.Eat()   // "Animal eats"
}

🐸 メリット

  • 共通処理を一箇所にまとめられる
  • コード重複を減らせる

🐘 デメリット

  • 親の変更が子に波及しやすい
  • 「is-a」関係が曖昧だと壊れやすい

🐼 Goは多重継承ではなく「多重埋め込み」

Goにはclassextendsが存在しません。
その代わりに構造体を複数埋め込むことで再利用できます。

type Animal struct{}
func (Animal) Eat() { fmt.Println("Animal eats") }

type Walker struct{}
func (Walker) Walk() { fmt.Println("Walks") }

type Dog struct {
    Animal
    Walker
}

func main() {
    d := Dog{}
    d.Eat()  // Animalのメソッド
    d.Walk() // Walkerのメソッド
}

🐤 Goでは「is-a」ではなく「has-a」。
継承ではなく、合成(composition)で再利用する発想です。


🐰 他言語の“クラシカル継承”との違い

class Animal {
    void eat() { System.out.println("Eating..."); }
}

class Dog extends Animal { // is-a 関係
    void bark() { System.out.println("Bark!"); }

    @Override
    void eat() { System.out.println("Dog eats meat"); }
}
できること 内容
単一継承 1つのスーパークラスだけ継承できる
メソッド追加 サブクラスに独自メソッドを追加できる
上書き 同シグネチャで親メソッドを上書きできる
ポリモーフィズム Animal a = new Dog(); a.eat(); // Dog eats meat が可能

🐸 親型として扱っても実際には子の振る舞いになる。
これが ポリモーフィズム(多態性)


🐼 Goとの比較

比較観点 Go Java/C#
継承構文 なし(埋め込み) extends / :
型関係 has-a(委譲) is-a(継承)
上書き 同名定義で優先 override明示的
ポリモーフィズム interfaceで実現 継承で実現
サブタイプ化 メソッド構造で判断 継承構造で判断

🐘 Goは「構造が同じなら代用できる」=構造的部分型を採用。
クラス階層を持たずに柔軟なOOPを実現しています。


🐤 インターフェースとは — 契約(約束)の定義

インターフェースは「この関数を持っているなら同じように扱える」という契約です。

type Eater interface {
    Eat()
}

type Dog struct{}
func (Dog) Eat() { fmt.Println("Dog eats") }

type Cat struct{}
func (Cat) Eat() { fmt.Println("Cat eats") }

func FeedAll(eaters []Eater) {
    for _, e := range eaters {
        e.Eat() // 型を気にせず呼べる!
    }
}

🐰 DogCat も同じ契約(Eat())を守っているので、
FeedAll() はどんな動物でも動かせる。
型ではなく、振る舞いで統一できるのが強みです。


⚖️ 違いを整理するとこうなる

観点 継承(スーパークラス) インターフェース
目的 実装の共有 仕様・契約の共通化
内容 実装+定義 定義のみ
依存関係 親実装に強く依存 契約のみに依存(疎結合)
Goでの実現 構造体埋め込み interface
関係性 is-a 必須 振る舞い一致でOK

🐼 使い分けの指針

状況 選ぶべき設計
共通の実装を再利用したい 継承(構造体埋め込み)
共通のルールで扱いたい インターフェース
is-a が成り立たない関係 継承ではなく委譲/インターフェース

🐘 継承は「どう動くか」を共有する。
🐤 インターフェースは「どう扱われるか」を保証する。


🐘 インターフェース変更の落とし穴 — “契約”を壊すと全部壊れる

Goでは、インターフェースを「実装します」と明示的に宣言しません。
メソッドのシグネチャが一致している型は自動的にそのインターフェースを満たす =「暗黙的実装」と呼ばれます。

つまり、インターフェースの中身を変えると、それを実装していたすべての型が壊れます

🐤 例:Eaterに新しい契約を足したら?

type Eater interface {
    Eat()
}

type Dog struct{}
func (Dog) Eat() { fmt.Println("Dog eats") }

// ✅ ここまではOK

// でも、インターフェースを拡張して…
type Eater interface {
    Eat()
    Sleep()  // ← 新しく追加
    Walk()
}
func FeedAll(eaters []Eater) {
    for _, e := range eaters {
        e.Eat()
    }
}

func main() {
    dogs := []Eater{Dog{}} // ❌ コンパイルエラー!
}

エラー内容:

Dog does not implement Eater (missing Sleep method)

🐶 正しく動かすには、全メソッドを実装し直す必要がある

type Dog struct{}
func (Dog) Eat()   { fmt.Println("Dog eats") }
func (Dog) Sleep() { fmt.Println("Dog sleeps") }
func (Dog) Walk()  { fmt.Println("Dog walks") }

これで FeedAll([]Eater{Dog{}}) は再び動作します。


💡 教訓:「契約は慎重に増やそう」

  • Goのinterfaceは“契約の完全一致”がルール
  • 契約(メソッド)を1つ追加しただけで、全実装が壊れることもある
  • フィールドを持てない(メソッドのみ)ので、“データ構造”ではなく“振る舞いの型”として使う

🪶 小さなコツ:インターフェースは細かく分ける

大きな「万能インターフェース」を1つ作るよりも、
小さな役割ごとのインターフェースを組み合わせるのがGoらしい設計です。

type Eater interface { Eat() }
type Walker interface { Walk() }

type Dog struct{}
func (Dog) Eat() { fmt.Println("Dog eats") }
func (Dog) Walk() { fmt.Println("Dog walks") }

func Play(e Eater, w Walker) {
    e.Eat()
    w.Walk()
}

こうすれば、インターフェース変更の影響を最小限にできる。
「小さな契約」をたくさん組み合わせるのが〇


🐰 まとめ

概念 意味 設計の目的
継承 親の実装を子が再利用 コード量削減・共通化
インターフェース 共通の契約を定義 柔軟な差し替え・拡張性

🐼 継承=再利用の仕組み
🐰 インターフェース=契約の仕組み

「再利用」と「契約」を分けて考えることで、
壊れにくく・テストしやすい設計 に近づきます。


🐤 関連記事

Discussion