コンポジットパターン
CoddyのJavaジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 75/87。
Composite Patternは、オブジェクトをツリー構造に組み合わせて、部分と全体の階層を表現できる構造設計パターンです。共通のインターフェースを通じて、個々のオブジェクトとオブジェクトの集合をクライアントが同じように扱えるようにします。
ファイルシステムを考えてみましょう。フォルダーにはファイルや他のフォルダーを含めることができ、それらの中にさらにファイルやフォルダーを含めることができます。単一のファイルを扱う場合でも、フォルダー全体の構造を扱う場合でも、「サイズを取得」のような操作を同じ方法で実行したいはずです。このパターンでは、Componentインターフェース、Leafオブジェクト(個々の要素)、Compositeオブジェクト(子要素を保持するコンテナー)を定義します。
interface FileComponent {
void display(String indent);
int getSize();
}
class File implements FileComponent {
private String name;
private int size;
public File(String name, int size) {
this.name = name;
this.size = size;
}
public void display(String indent) {
System.out.println(indent + name + " (" + size + "KB)");
}
public int getSize() {
return size;
}
}
class Folder implements FileComponent {
private String name;
private List<FileComponent> children = new ArrayList<>();
public Folder(String name) {
this.name = name;
}
public void add(FileComponent component) {
children.add(component);
}
public void display(String indent) {
System.out.println(indent + name + "/");
for (FileComponent child : children) {
child.display(indent + " ");
}
}
public int getSize() {
return children.stream().mapToInt(FileComponent::getSize).sum();
}
}composite は children に操作を委譲し、再帰的な構造を可能にします。クライアントは、リーフと composite のどちらを扱っているのかを知ることなく、ツリーとやり取りできます。
Folder root = new Folder("Documents");
root.add(new File("resume.pdf", 150));
Folder photos = new Folder("Photos");
photos.add(new File("vacation.jpg", 2000));
root.add(photos);
root.display("");
System.out.println("Total: " + root.getSize() + "KB");Composite Patternは、組織図、UIコンポーネント、メニューシステムなど、グループと個々の要素を同じように扱う必要がある階層構造を表現するのに最適です。
チャレンジ
簡単Composite Pattern を使って組織図システムを構築しましょう!departments が employees やサブDepartmentを含められる構造を作り、単一のemployeeを見ている場合でもdivision全体を見ている場合でも、合計給与を計算し、階層を統一的に表示できるようにします。
コードを4つのファイルに分けて整理します。
OrganizationComponent.java:employees と departments の両方が実装する component インターフェースをDefineします。2つのメソッドをDeclareする必要があります。showDetails(String indent)は適切なインデントで component を表示し、getSalary()は合計給与を整数として返します。Employee.java:個々のworkerを表すleaf classをCreateします。Employeeは name(String)と salary(int)を持ち、どちらもconstructorを通じて設定されます。showDetailsがcallされると、[indent][name]: $[salary]をprintする必要があります。getSalarymethodはemployeeのsalaryをそのまま返します。Department.java:employees とサブdepartmentsの両方を保持できるcomposite classをCreateします。Departmentは name(String)を持ち、OrganizationComponentのchildrenのlistを管理します。membersを追加するためのadd(OrganizationComponent component)methodを持つ必要があります。showDetailsがcallされると、[indent][name] Departmentをprintし、その後、各childに対してインデントを増やして(スペースを2つ追加して)showDetailsをcallします。getSalarymethodは、すべてのchildrenのsalaryの合計を返します。Main.java:組織を構築します!4つの入力を受け取ります。salary付きのemployee nameが2つと、1人のemployeeを持つサブDepartment nameです。rootとして "Engineering" DepartmentをCreateします。最初の2つのname/salaryの組を使って、2人のemployeesを追加します。次に、3番目の入力を使ってサブDepartmentをCreateし、4番目のname/salaryの組を使って1人のemployeeを追加して、このサブDepartmentをEngineeringに追加します。
Engineering Departmentで
showDetails("")をcallし、その後、合計給与をTotal Salary: $[amount]のformatでprintします。
入力は次の順序で受け取ります:employee1 name(String)、employee1 salary(int)、employee2 name(String)、employee2 salary(int)、サブDepartment name(String)、employee3 name(String)、employee3 salary(int)。
たとえば、入力が Alice、75000、Bob、65000、QA、Charlie、55000 の場合、出力は次のようになります。
Engineering Department
Alice: $75000
Bob: $65000
QA Department
Charlie: $55000
Total Salary: $195000単一のemployeeに対してcallした場合でも、ネストされたサブdepartmentsを含むDepartment全体に対してcallした場合でも、同じ showDetails と getSalary methodがシームレスに機能することに注目してください。composite structureがrecursionを自動的に処理するため、階層全体を統一的に扱うことができます!
自分で試してみよう
import java.util.Scanner;
class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 入力を読み取る
String emp1Name = scanner.nextLine();
int emp1Salary = Integer.parseInt(scanner.nextLine());
String emp2Name = scanner.nextLine();
int emp2Salary = Integer.parseInt(scanner.nextLine());
String subDeptName = scanner.nextLine();
String emp3Name = scanner.nextLine();
int emp3Salary = Integer.parseInt(scanner.nextLine());
// TODO: Create the "Engineering" department as the root
// TODO: emp1とemp2のデータを使用してEngineeringに2人の従業員を作成して追加する
// TODO: subDeptNameを使用してサブ部署を作成する
// TODO: emp3のデータを使用してサブ部署に1人の従業員を作成して追加する
// TODO: サブ部署をEngineeringに追加する
// TODO: Call showDetails("") on the Engineering department
// TODO: 次の形式で合計給与を出力する: Total Salary: $[amount]
}
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
4継承
継承の基本 (extends)super キーワードメソッドのオーバーライド (@Override)コンストラクタチェーンObject クラス単一継承と多段階継承なぜ多重継承ができないのかまとめ:従業員の階層構造自分で練習してみよう: Javaオンラインコンパイラ