コンポジットパターン
CoddyのC#ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 61/70。
Composite パターンは、オブジェクトをツリー構造に構成し、個々のオブジェクトとオブジェクトのグループを一貫して扱うことができる構造パターンです。単一のアイテムとコレクションの両方を同じインターフェースを通じて操作したい、部分-全体階層を表現するのに最適です。
このパターンには、3つの主要なコンポーネントがあります。共通の操作を定義するコンポーネントインターフェース、個々のオブジェクトを表すリーフクラス、そして子を持ち、同じインターフェースを実装するコンポジットクラスです。
public interface IFileSystemItem
{
string Name { get; }
int GetSize();
}
public class File : IFileSystemItem
{
public string Name { get; }
private int _size;
public File(string name, int size)
{
Name = name;
_size = size;
}
public int GetSize() => _size;
}
public class Folder : IFileSystemItem
{
public string Name { get; }
private List<IFileSystemItem> _items = new List<IFileSystemItem>();
public Folder(string name) => Name = name;
public void Add(IFileSystemItem item) => _items.Add(item);
public int GetSize() => _items.Sum(item => item.GetSize());
}このパターンの素晴らしい点は、クライアントが単一のファイルを作業対象としているのか、それともフォルダ階層全体を作業対象としているのかを知る必要がないことです:
var root = new Folder("root");
root.Add(new File("doc.txt", 100));
var subFolder = new Folder("images");
subFolder.Add(new File("photo.jpg", 500));
subFolder.Add(new File("icon.png", 50));
root.Add(subFolder);
Console.WriteLine(root.GetSize()); // 650Compositeパターンは、ファイルシステム、UIコンポーネントツリー、組織図、あるいはコンテナとコンテンツを同一に扱う必要があるあらゆる構造に最適です。単純な要素と複雑な要素を区別する必要をなくすことで、クライアントコードを簡素化します。
チャレンジ
簡単Composite パターンを使用して、組織図システムを構築しましょう。個々の従業員と、従業員や他の部門を含む部門の両方を一律に扱うことができる構造を作成し、階層のどのレベルでも総給与コストを計算できるようにします。
コードは 3 つのファイルに分けて構成します:
OrgComponent.cs:Organization名前空間にIOrgComponentインターフェースを定義します。これには、Nameプロパティ(string)と、整数を返すGetSalary()メソッドの 2 つのメンバーを含めます。このインターフェースは、個々の従業員と部門の両方が従う共通のコントラクトを表します。OrgClasses.cs: 同じ名前空間に 2 つのクラスを作成します:Employee- 個々の労働者を表すリーフ(末端)クラスです。コンストラクタで名前と給与を受け取り、自身の給与を返すことでインターフェースを実装します。Department- グループを表すコンポジット(複合)クラスです。コンストラクタで名前を受け取り、IOrgComponentアイテムのリストを保持します。メンバーを追加するためのAdd(IOrgComponent component)メソッドを含めます。そのGetSalary()は、すべてのメンバーの給与の合計を返す必要があります。
Program.cs: 入力に基づいて組織構造を構築し、総給与を計算します。部門と従業員を作成し、それらを適切に入れ子にして、Composite パターンによって階層全体を一律に扱えることを示します。
以下の入力を受け取ります:
- 作成するコンポーネントの数
- 各コンポーネントについて:タイプ(
employeeまたはdepartment)、続いて名前、従業員の場合はその給与。部門の場合は、親部門の名前(トップレベルの部門の場合はroot)も受け取ります。
各コンポーネントの入力形式:
employee
{name}
{salary}
{parent_department}
department
{name}
{parent_department}構造を構築した後、ルート部門の総給与を {root_name} Total Salary: {amount} の形式で出力してください。
例えば、入力が以下の場合:
5
department
Engineering
root
employee
Alice
5000
Engineering
employee
Bob
4500
Engineering
department
QA
Engineering
employee
Charlie
4000
QA出力は以下のようになります:
Engineering Total Salary: 13500Engineering 部門には、Alice (5000)、Bob (4500)、および QA 部門が含まれています。QA には Charlie (4000) が含まれています。Engineering に対して GetSalary() を呼び出すと、入れ子になったすべてのコンポーネントから合計が再帰的に計算されます。これは、Composite パターンがいかにシンプルで統一されたインターフェースを通じて複雑なツリー構造を扱えるかを示しています!
チートシート
Composite パターンは、オブジェクトをツリー構造に構成し、個々のオブジェクトとグループを同じインターフェースを通じて一貫して扱う構造に関するパターンです。これは「部分-全体階層」を表現するのに最適です。
このパターンには3つの主要なコンポーネントがあります:
- コンポーネントインターフェース:共通の操作を定義します
- Leaf(葉)クラス:個々のオブジェクトを表します
- Composite(複合)クラス:子要素を保持し、同じインターフェースを実装します
基本的な実装例:
// コンポーネントインターフェース
public interface IFileSystemItem
{
string Name { get; }
int GetSize();
}
// Leafクラス
public class File : IFileSystemItem
{
public string Name { get; }
private int _size;
public File(string name, int size)
{
Name = name;
_size = size;
}
public int GetSize() => _size;
}
// Compositeクラス
public class Folder : IFileSystemItem
{
public string Name { get; }
private List<IFileSystemItem> _items = new List<IFileSystemItem>();
public Folder(string name) => Name = name;
public void Add(IFileSystemItem item) => _items.Add(item);
public int GetSize() => _items.Sum(item => item.GetSize());
}使用例 - クライアントは単一のアイテムとコレクションを一貫して扱います:
var root = new Folder("root");
root.Add(new File("doc.txt", 100));
var subFolder = new Folder("images");
subFolder.Add(new File("photo.jpg", 500));
subFolder.Add(new File("icon.png", 50));
root.Add(subFolder);
Console.WriteLine(root.GetSize()); // 650Composite パターンは、ファイルシステム、UIコンポーネントツリー、組織図、またはコンテナと中身を同じように扱う必要があるあらゆる構造に役立ちます。
自分で試してみよう
using System;
using System.Collections.Generic;
using Organization;
class Program
{
public static void Main(string[] args)
{
int n = Convert.ToInt32(Console.ReadLine());
// TODO: 部署を名前で保存するための辞書を作成する
// これにより、コンポーネントを追加する際に親部署を見つけやすくなります
Dictionary<string, Department> departments = new Dictionary<string, Department>();
// TODO: ルート部署を追跡する
Department root = null;
for (int i = 0; i < n; i++)
{
string type = Console.ReadLine();
if (type == "department")
{
string name = Console.ReadLine();
string parent = Console.ReadLine();
// TODO: 部署を作成する
// TODO: それを departments 辞書に追加する
// TODO: parent が "root" の場合、これがルート部署になります
// TODO: それ以外の場合は、親部署を見つけて、この部署をそこに追加します
}
else if (type == "employee")
{
string name = Console.ReadLine();
int salary = Convert.ToInt32(Console.ReadLine());
string parent = Console.ReadLine();
// TODO: 従業員を作成する
// TODO: 親部署を見つけて、従業員をそこに追加します
}
}
// TODO: ルート部署の総給与を出力する
// フォーマット: "{root_name} Total Salary: {amount}"
Console.WriteLine($"{root.Name} Total Salary: {root.GetSalary()}");
}
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。