Menu
CoddyTech

コンポジットパターン

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

コンポジットパターンは、オブジェクトをツリー構造に組み合わせて、全体と部分の階層を表現できる構造設計パターンです。共通のインターフェースを通じて、個々のオブジェクトとオブジェクトの集合をクライアントが同じように扱えるようにします。

ファイルシステムを考えてみましょう。フォルダーにはファイルやほかのフォルダーを格納でき、その中にもさらにファイルやフォルダーを格納できます。単一のファイルを扱う場合でも、フォルダー構造全体を扱う場合でも、「サイズを取得する」のような操作を同じ方法で行いたいものです。このパターンでは、コンポーネントインターフェース、リーフオブジェクト(個々の要素)、そして子要素を保持するコンテナーであるコンポジットオブジェクトを定義します。

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();
    }
}

コンポジットは操作を子要素に委譲し、再帰的な構造を可能にします。クライアントは、葉またはコンポジットのどちらを扱っているかを知ることなく、ツリーを操作できます。

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

challenge icon

チャレンジ

簡単

Composite Patternを使って組織図システムを作りましょう!部署が従業員や他の下位部署を含められる構造を作成します。これにより、単独の従業員でも部門全体でも、給与の合計を計算し、階層を同じように表示できます。

コードを4つのファイルに分けて作成します。

  • OrganizationComponent.java: 従業員と部署の両方が実装するコンポーネントインターフェースを定義します。2つのメソッドを宣言します。適切なインデントでコンポーネントを表示するshowDetails(String indent)と、合計給与を整数で返すgetSalary()です。
  • Employee.java: 個々の従業員を表すリーフクラスを作成します。Employeeは名前(String)と給与(int)を持ち、どちらもコンストラクターで設定します。showDetailsが呼び出されたときは、[indent][name]: $[salary]を出力します。getSalaryメソッドは従業員の給与をそのまま返します。
  • Department.java: 従業員と下位部署の両方を格納できるコンポジットクラスを作成します。Departmentは名前(String)を持ち、子要素であるOrganizationComponentのリストを保持します。メンバーを追加するadd(OrganizationComponent component)メソッドを用意します。showDetailsが呼び出されたときは、[indent][name] Departmentを出力し、その後、各子要素に対してインデントを増やして(スペースを2つ追加して)showDetailsを呼び出します。getSalaryメソッドは、すべての子要素の給与の合計を返します。
  • Main.java: 組織を作りましょう!従業員2人の名前と給与、そして下位部署の名前と従業員1人の情報という、4つの入力を受け取ります。 ルートとして"Engineering"部署を作成します。最初の2組の名前と給与を使って、従業員2人をその部署に追加します。次に、3番目の入力を使って下位部署を作成し、4組目の名前と給与を使って従業員1人を追加した後、この下位部署をEngineeringに追加します。

    Engineering部署に対してshowDetails("")を呼び出し、その後、合計給与をTotal Salary: $[amount]の形式で出力します。

入力は次の順番で受け取ります。employee1の名前(String)、employee1の給与(int)、employee2の名前(String)、employee2の給与(int)、下位部署の名前(String)、employee3の名前(String)、employee3の給与(int)。

たとえば、入力がAlice、75000、Bob、65000、QA、Charlie、55000の場合、出力は次のようになります。

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

showDetailsとgetSalaryの同じメソッドが、単独の従業員に対しても、下位部署を含む部署全体に対しても、シームレスに動作することに注目してください。コンポジット構造が再帰処理を自動的に担うため、階層全体を同じように扱えます!

自分で試してみよう

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: "Engineering" 部門をルートとして作成する
        
        // TODO: emp1 と emp2 のデータを使って従業員を2人作成し、Engineering に追加する
        
        // TODO: subDeptName を使ってサブ部門を作成する
        
        // TODO: emp3 のデータを使って従業員を1人作成し、サブ部門に追加する
        
        // TODO: サブ部門を Engineering に追加する
        
        // TODO: Engineering 部門で showDetails("") を呼び出す
        
        // TODO: 給与総額を Total Salary: $[amount] の形式で出力する
    }
}
quiz icon腕試し

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

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

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