埋め込み vs 集約
CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 38/107。
埋め込みによって、フィールドやメソッドが外側の構造体に昇格されることを学びました。しかし、Goは集約(aggregation)もサポートしています。これは、別の構造体を名前付きフィールドとして含める手法です。それぞれの方法をいつ使用すべきかを理解することは、優れた設計の鍵となります。
埋め込みを使用すると、フィールド名を省略でき、内部型のメンバに直接アクセスできるようになります。
type Writer struct{}
func (w Writer) Write() string { return "writing" }
type Document struct {
Writer // 埋め込み - フィールド名なし
}
d := Document{}
d.Write() // 直接アクセス
集約では、フィールドに明示的な名前を付けます。内部の型にアクセスするには、その名前を使用する必要があります:
type Document struct {
writer Writer // 集約 - 名前付きフィールド
}
d := Document{writer: Writer{}}
d.writer.Write() // フィールド名を使用する必要がある
主な違いは、表現しようとしている関係性にあります。埋め込み(Embedding)は、外部の型が内部の型のアイデンティティを引き継ぐ「is-a」関係を示唆します。集約(Aggregation)は、外部の型が単に内部の型を保持するだけの「has-a」関係を示唆します。
外側の型に内側の型が実装しているインターフェースを満たさせたい場合や、直接的なメソッドアクセスによってAPIがよりクリーンになる場合は、埋め込みを使用します。内側の型が公開すべきではない実装の詳細である場合や、内側の型へのアクセス方法を明示的に制御したい場合は、集約を使用します。
チャレンジ
簡単埋め込み(embedding)と集約(aggregation)をいつ使い分けるかを示すレポート生成システムを構築しましょう。一部のコンポーネントは直接アクセス可能(埋め込み)にし、他のコンポーネントは内部の実装詳細(集約)として保持するシステムを作成します。
コードを3つのファイルに分けて構成します:
formatter.go:Styleフィールド(string)を持つFormatter構造体と、テキストをスタイルで囲んで[Style] text [/Style]のように返すFormat(text string) stringメソッドを作成します。これは直接公開すべきではない内部ヘルパーになります。report.go: レポート生成を異なる方法で処理する2つの構造体を作成します:AuthorとDateフィールドを持つMetadata構造体 — これは直接アクセス可能であるべき公開情報を表します。Titleフィールドを持つReport構造体 —Metadataを埋め込み(著者と日付に直接アクセスできるようにするため)、Formatterには集約(formatterという名前付きフィールドを持つ内部実装詳細として)を使用します。
Generate() stringメソッドをReportに追加します。main.go: 入力からレポートの詳細を読み取り、埋め込まれたメタデータと集約されたフォーマッタの両方を持つReportを作成します。レポートから直接著者にアクセスすること(埋め込みによる)と、内部で隠されたフォーマッタを使用する整形済みレポートを生成することの違いを示します。
以下の入力が提供されます:
- 1行目: レポートのタイトル
- 2行目: 著者名
- 3行目: 日付
- 4行目: フォーマッタのスタイル
Format メソッドは以下を返す必要があります:
[Style] text [/Style]Generate メソッドは以下を返す必要があります:
[formatted title]
Author: [Author], Date: [Date]メインファイルでは、以下の3行を出力してください:
- Report から直接アクセスした著者(埋め込みによってフィールドが昇格することを示します)
- 埋め込まれた型名を通じてアクセスした日付(
report.Metadata.Date) Generate()を呼び出した結果
例えば、Quarterly Results、Jane Doe、2024-01-15、BOLD が与えられた場合、出力は以下のようになります:
Jane Doe
2024-01-15
[BOLD] Quarterly Results [/BOLD]
Author: Jane Doe, Date: 2024-01-15埋め込みによって Author と Date が Report から直接アクセスできる一方で、フォーマッタは隠されたままであることに注目してください。名前付きフィールドで集約されているため、report.Format() を直接呼び出すことはできません。これは、実践における「is-a」関係と「has-a」関係の違いを示しています。
チートシート
Goは、ある構造体を別の構造体に含めるための2つの方法をサポートしています:埋め込み(embedding)と集約(aggregation)です。
埋め込みはフィールド名を省略し、内部型のフィールドとメソッドを外部構造体に昇格(promote)させます:
type Writer struct{}
func (w Writer) Write() string { return "writing" }
type Document struct {
Writer // 埋め込み - フィールド名なし
}
d := Document{}
d.Write() // 埋め込まれたメソッドへの直接アクセス
集約は明示的なフィールド名を使用し、内部型にアクセスするためにその名前を必要とします:
type Document struct {
writer Writer // 集約 - 名前付きフィールド
}
d := Document{writer: Writer{}}
d.writer.Write() // フィールド名を使用する必要がある
それぞれの使い分け:
- 埋め込み:「is-a」関係を表します。外部型が内部型のインターフェースを満たす必要がある場合や、メソッドへの直接アクセスによってAPIが改善される場合に使用します。
- 集約:「has-a」関係を表します。内部型が公開すべきではない実装の詳細である場合や、アクセスを明示的に制御したい場合に使用します。
埋め込みを使用しても、必要に応じて埋め込まれた型名を通じてフィールドにアクセスできます:
type Metadata struct {
Author string
}
type Report struct {
Metadata // 埋め込み
}
r := Report{Metadata: Metadata{Author: "Jane"}}
r.Author // 昇格による直接アクセス
r.Metadata.Author // 型名による明示的なアクセス
自分で試してみよう
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
title := scanner.Text()
scanner.Scan()
author := scanner.Text()
scanner.Scan()
date := scanner.Text()
scanner.Scan()
style := scanner.Text()
// TODO: authorとdateを持つMetadata構造体を作成する
// TODO: 指定されたstyleを持つFormatterを作成する
// TODO: Metadataを埋め込み、Formatterを集約するReportを作成する
// TODO: Reportから直接アクセスしてauthorを出力する(埋め込みによってフィールドが昇格される)
// TODO: 埋め込まれた型名を通じてアクセスしてdateを出力する(report.Metadata.Date)
// TODO: Generate()を呼び出した結果を出力する
// 未使用変数に関する警告を抑制する(変数を使用する際にこれらを削除してください)
_ = title
_ = author
_ = date
_ = style
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー