Goに継承がない理由
CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 34/107。
JavaやC++のような伝統的なオブジェクト指向言語は、型の間でコードを共有するために継承を使用します。子クラスは親クラスを拡張し、そのすべてのフィールドとメソッドを継承します。Goは意図的にこの機能を省いています。
継承は型同士の間に密結合を生み出します。親クラスが変更されると、すべての小クラスが影響を受けます。
深い継承階層は、理解やメンテナンスが困難になります。「壊れやすい基底クラスの問題(fragile base class problem)」は、基底クラスへの変更が派生クラスを予期せず破損させたときに発生します。
Goの設計者は異なる道を選びました:継承よりもコンポジション。「犬は動物である」と言う代わりに、Goは「犬は動物のような振る舞いを持っている」と言うことを推奨しています。この微妙な転換が、より柔軟でメンテナンス性の高いコードにつながります。
Goは、すでに学んだ2つのメカニズムを通じてコードの再利用を実現します。
- インターフェースは、実装の詳細を持たずに振る舞いの契約を定義します
- 構造体の埋め込みにより、型が他の型を含み、そのメソッドを再利用できるようになります。
この比較を考えてみましょう。伝統的なOOPでは、class Dog extends Animalのように記述するかもしれません。Goでは、Animal構造体をDogの中に埋め込み、共通のインターフェースを実装します。その結果、型同士の結合がより緩やかになりながら、同様の機能が得られます。
この章では、構造体の埋め込み(struct embedding)について深く掘り下げ、Goが継承の欠点を伴わずにその利点をどのように実現しているかを示します。
チャレンジ
簡単継承の代わりに Go のコンポジション(合成)アプローチを実証する通知システムを構築しましょう。クラス階層ではなく、インターフェースと構造体の埋め込み(struct embedding)を通じて動作を共有する型を作成します。
コードは以下の3つのファイルに分けて構成します:
notifier.go:Notify(message string) stringという単一のメソッドを持つNotifierインターフェースを定義します。また、他の型に埋め込まれるNameフィールドを持つBaseNotifier構造体を作成します。BaseNotifierには、メッセージの前にブラケットで囲んだ通知者名を付加して返すFormat(message string) stringというメソッドを実装してください。channels.go:BaseNotifierを埋め込み、Notifierインターフェースを実装する2つの通知チャネル型を作成します:- 追加の
Addressフィールドを持つEmailNotifier - 追加の
Phoneフィールドを持つSMSNotifier
Notifyメソッドは、埋め込まれたFormatメソッドを使用し、出力にそれぞれのチャネル固有の情報を含める必要があります。- 追加の
main.go: 任意のNotifierとメッセージを受け取り、Notifyを呼び出した結果を返すSendAlertという関数を作成します。入力から通知の詳細を読み取り、両方の型の通知者を作成し、インターフェースを通じてそれらを互換的に使用できることを示してください。
以下の入力が提供されます:
- 1行目: メール通知者の名前
- 2行目: メールアドレス
- 3行目: SMS通知者の名前
- 4行目: 電話番号
- 5行目: アラートメッセージ
BaseNotifier の Format メソッドは以下を返す必要があります:
[Name] message各 Notify メソッドは以下を返す必要があります:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
例えば、Alerts、user@mail.com、Urgent、555-1234、Server down が与えられた場合、出力は以下のようになります:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server down両方の通知者型が埋め込みを通じて BaseNotifier から Format メソッドを再利用しつつ、それぞれが独自の Notify 実装を提供していることに注目してください。SendAlert 関数は、具体的な型を知ることなく、任意の Notifier で動作します。これが「継承よりもコンポジション」を実践した形です。
チートシート
Goは、型の間でコードを共有するために継承よりもコンポジションを使用し、従来のオブジェクト指向の継承における密結合や壊れやすい基底クラスの問題を回避します。
クラス階層の代わりに、Goは以下を通じてコードの再利用を実現します:
- インターフェース - 実装の詳細なしで振る舞いの契約を定義します
- 構造体の埋め込み - 型が他の型を含み、そのメソッドを再利用できるようにします
「犬は動物である」(継承)と言うのではなく、Goは「犬は動物のような振る舞いを持っている」(コンポジション)という考え方を推奨します。
構造体の埋め込みの例
埋め込まれるベースとなる構造体を定義します:
type BaseNotifier struct {
Name string
}
func (b BaseNotifier) Format(message string) string {
return "[" + b.Name + "] " + message
}フィールドとメソッドを再利用するために、ベースとなる構造体を他の型に埋め込みます:
type EmailNotifier struct {
BaseNotifier // 埋め込まれた構造体
Address string
}
func (e EmailNotifier) Notify(message string) string {
// 埋め込まれたFormatメソッドを直接呼び出すことができます
return "Email to " + e.Address + ": " + e.Format(message)
}複数の型が実装できるインターフェースを定義します:
type Notifier interface {
Notify(message string) string
}インターフェースを使用して、異なる具体的な型を互換的に操作します:
func SendAlert(n Notifier, message string) string {
return n.Notify(message)
}このアプローチは継承と同様の機能を提供しますが、型同士の結合はより緩やかになります。
自分で試してみよう
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert は任意の Notifier とメッセージを受け取り、Notify を呼び出した結果を返します
// TODO: SendAlert 関数を実装してください
func main() {
scanner := bufio.NewScanner(os.Stdin)
// メールの通知者名を読み込む
scanner.Scan()
emailName := scanner.Text()
// メールアドレスを読み込む
scanner.Scan()
emailAddress := scanner.Text()
// SMS の通知者名を読み込む
scanner.Scan()
smsName := scanner.Text()
// 電話番号を読み込む
scanner.Scan()
phoneNumber := scanner.Text()
// アラートメッセージを読み込む
scanner.Scan()
alertMessage := scanner.Text()
// TODO: emailName と emailAddress を使用して EmailNotifier を作成してください
// TODO: smsName と phoneNumber を使用して SMSNotifier を作成してください
// TODO: SendAlert を使用して両方の通知者から alertMessage を送信し、
// その結果をプリントしてください
// 未使用変数の警告を抑制(変数を使用する際にこれらを削除してください)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー