Menu
Coddy logo textTech

Compositeパターン

CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 96/107。

Composite パターンを使用すると、個々のオブジェクトとオブジェクトのグループを一貫して扱うことができます。State パターンが内部状態に基づいて振る舞いを変えるのに対し、Composite は、末端(リーフ)とコンテナの両方が同じインターフェースを共有するツリー構造を構築します。

このパターンは、ファイルシステム、組織図、UIコンポーネントのような階層構造に最適です。Goでは、個々のアイテムとコンテナの両方が実装する共通のインターフェースを定義します:

type Component interface {
    GetSize() int
}

type File struct {
    Name string
    Size int
}

func (f File) GetSize() int {
    return f.Size
}

コンポジット(コンテナ)は子要素を保持し、それらの結果を集約することで同じインターフェースを実装します:

type Folder struct {
    Name     string
    Children []Component
}

func (f *Folder) Add(c Component) {
    f.Children = append(f.Children, c)
}

func (f Folder) GetSize() int {
    total := 0
    for _, child := range f.Children {
        total += child.GetSize()
    }
    return total
}

これで、フォルダの中にフォルダを入れ子にすることができるようになり、単一のファイルであってもディレクトリツリー全体であっても、GetSize() の呼び出しは同様に動作します:

docs := &Folder{Name: "docs"}
docs.Add(File{Name: "readme.txt", Size: 100})
docs.Add(File{Name: "notes.txt", Size: 50})

root := &Folder{Name: "root"}
root.Add(docs)
root.Add(File{Name: "config.json", Size: 25})

fmt.Println(root.GetSize())  // 175

Compositeは、部分-全体階層を表現する必要があり、クライアントが個々のオブジェクトと合成物を同様に扱いたい場合に最適です。

challenge icon

チャレンジ

簡単

Composite パターンを使用して、組織図システムを構築しましょう!個々の従業員と、従業員や他の部門を含む部門の両方を一律に扱うことができる構造を作成します。これは、組織のあらゆる部分で合計給与を計算するのに最適です。

コードは 3 つのファイルに分けて構成します:

  • component.go: 個人とグループの両方が実装する共通のインターフェースを定義します。

    2 つのメソッドを持つ OrgComponent インターフェースを作成します:

    • GetSalary() int — このコンポーネントの合計給与を返します
    • GetName() string — 従業員または部門の名前を返します
  • organization.go: リーフ(個々の従業員)とコンポジット(部門)の両方の型を実装します。

    Name (string) と Salary (int) フィールドを持つ Employee 構造体を作成します。その GetSalary() は自身の給与を返し、GetName() は自身の名前を返します。

    Name (string) フィールドと、OrgComponent アイテムを保持する Members スライスを持つ Department 構造体を作成します。メンバーを追加するための Add(c OrgComponent) メソッドを追加します。その GetSalary() はすべてのメンバーの給与の合計を返し、GetName() は部門名を返します。

  • main.go: 組織構造を構築し、給与を計算します。

    従業員の数を読み取ります。各従業員について、名前と給与を読み取り、Employee インスタンスを作成します。次に、部門の数を読み取ります。各部門について、その名前とメンバーのインデックス数、続いてそれらのインデックス(作成順に、以前に作成された従業員または部門を指す 0 から始まる数値)を読み取ります。

    構造を構築した後、最後のインデックスを読み取り、そのコンポーネントの名前と合計給与を [name]: [salary] の形式で出力します。

以下の入力が提供されます:

  • 従業員数、続いて各従業員の名前と給与(別々の行)
  • 部門数、続いて各部門の名前、メンバー数、およびメンバーのインデックス
  • クエリする最終インデックス

例えば、以下の場合:

3
Alice
50000
Bob
60000
Carol
55000
1
Engineering
3
0
1
2
3

出力は以下のようになります:

Engineering: 165000

また、以下の場合:

4
Alice
50000
Bob
60000
Carol
55000
Dave
70000
2
Dev
2
0
1
QA
2
2
3
4

出力は以下のようになります:

QA: 125000

また、以下の場合:

2
Alice
50000
Bob
60000
0
1

出力は以下のようになります:

Bob: 60000

GetSalary() が、単一の従業員に対して呼び出すか、部門全体に対して呼び出すかにかかわらず、同じように機能することに注目してください。Composite パターンを使用すると、同じインターフェースを通じて個人とグループを一律に扱うことができます!

チートシート

Composite パターンは、共通のインターフェースを通じて、個々のオブジェクトとオブジェクトのグループを一貫して扱います。ファイルシステム、組織図、UIコンポーネントのような階層構造に最適です。

個々のアイテムとコンテナの両方が実装する共通のインターフェースを定義します:

type Component interface {
    GetSize() int
}

リーフ(個々のアイテム)を実装します:

type File struct {
    Name string
    Size int
}

func (f File) GetSize() int {
    return f.Size
}

子要素を保持し、その結果を集計するコンポジット(コンテナ)を実装します:

type Folder struct {
    Name     string
    Children []Component
}

func (f *Folder) Add(c Component) {
    f.Children = append(f.Children, c)
}

func (f Folder) GetSize() int {
    total := 0
    for _, child := range f.Children {
        total += child.GetSize()
    }
    return total
}

個々のアイテムとコンポジットを一貫して扱う使用例:

docs := &Folder{Name: "docs"}
docs.Add(File{Name: "readme.txt", Size: 100})
docs.Add(File{Name: "notes.txt", Size: 50})

root := &Folder{Name: "root"}
root.Add(docs)
root.Add(File{Name: "config.json", Size: 25})

fmt.Println(root.GetSize())  // 175

自分で試してみよう

package main

import "fmt"

func main() {
	// 従業員数を読み込む
	var numEmployees int
	fmt.Scanln(&numEmployees)

	// すべてのコンポーネント(従業員と部門)を作成順に格納する
	var components []OrgComponent

	// TODO: 各従業員の名前と給与を読み込む
	// Employee インスタンスを作成し、components スライスに追加する
	for i := 0; i < numEmployees; i++ {
		var name string
		var salary int
		fmt.Scanln(&name)
		fmt.Scanln(&salary)
		// TODO: 従業員を作成し、components に追加する
	}

	// 部門数を読み込む
	var numDepartments int
	fmt.Scanln(&numDepartments)

	// TODO: 各部門の名前、メンバー数、およびメンバーのインデックスを読み込む
	// Department インスタンスを作成し、インデックスでメンバーを追加し、components に追加する
	for i := 0; i < numDepartments; i++ {
		var deptName string
		var memberCount int
		fmt.Scanln(&deptName)
		fmt.Scanln(&memberCount)
		// TODO: 部門を作成し、メンバーのインデックスを読み込み、メンバーを追加し、components に追加する
	}

	// クエリする最終的なインデックスを読み込む
	var queryIndex int
	fmt.Scanln(&queryIndex)

	// TODO: コンポーネントの名前と総給与を [name]: [salary] の形式で出力する
	// 例: fmt.Printf("%s: %d\n", name, salary)
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン