Menu
Coddy logo textTech

コンポジットパターン

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コンポーネント、メニューシステムなど、グループと個々の要素を同じように扱う必要がある階層構造を表現するのに最適です。

challenge icon

チャレンジ

簡単

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する必要があります。getSalary methodは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します。getSalary methodは、すべての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)。

たとえば、入力が Alice75000Bob65000QACharlie55000 の場合、出力は次のようになります。

Engineering Department
  Alice: $75000
  Bob: $65000
  QA Department
    Charlie: $55000
Total Salary: $195000

単一のemployeeに対してcallした場合でも、ネストされたサブdepartmentsを含むDepartment全体に対してcallした場合でも、同じ showDetailsgetSalary 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]
    }
}
quiz icon腕試し

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

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

自分で練習してみよう: Javaオンラインコンパイラ