Menu
Coddy logo textTech

コンポジットパターン

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

Composite パターンは、オブジェクトをツリー構造に構成して部分-全体階層を表現できるようにする構造に関するデザインパターンです。これにより、クライアントは共通のインターフェースを通じて、個々のオブジェクトとオブジェクトの合成物を一貫して扱うことができます。

ファイルシステムを考えてみましょう。フォルダにはファイルや他のフォルダを含めることができ、それらの中にさらにファイルやフォルダを含めることができます。単一のファイルを扱う場合でも、フォルダ構造全体を扱う場合でも、「サイズを取得する」といった操作を同じ方法で実行したいはずです。このパターンでは、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();
    }
}

コンポジットは操作をその子要素に委譲し、再帰的な構造を可能にします。クライアントは、リーフを扱っているのかコンポジットを扱っているのかを意識することなく、ツリーとやり取りします:

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パターンを使用して、組織図システムを構築しましょう!部署が従業員や他のサブ部署を含むことができる構造を作成し、単一の従業員であっても部門全体であっても、総給与の計算や階層の表示を一律に行えるようにします。

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

  • OrganizationComponent.java: 従業員と部署の両方が実装するコンポーネントインターフェースを定義します。適切なインデントでコンポーネントを表示するための showDetails(String indent) と、総給与を整数で返す getSalary() の2つのメソッドを宣言する必要があります。
  • 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: 組織を構築しましょう!4つの入力を受け取ります:2人の従業員の名前と給与、および1人の従業員を含むサブ部署の名前です。

    ルートとして "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

同じ showDetailsgetSalary メソッドが、単一の従業員に対して呼び出された場合でも、ネストされたサブ部署を含む部署全体に対して呼び出された場合でも、シームレスに機能することに注目してください。コンポジット構造が再帰を自動的に処理するため、階層全体を一律に扱うことができます!

チートシート

Compositeパターンは、オブジェクトをツリー構造に構成して「部分-全体」の階層を表現する構造に関するデザインパターンです。これにより、個々のオブジェクトとそれらを組み合わせたものを、共通のインターフェースを通じて一貫して扱うことができます。

このパターンは、主に3つの要素で構成されています:

  • Component: 共通の操作を定義するインターフェース
  • Leaf: Componentインターフェースを実装する個々のオブジェクト
  • Composite: 子要素を保持し、操作をそれらに委譲するコンテナオブジェクト

ファイルシステムの例:

// Componentインターフェース
interface FileComponent {
    void display(String indent);
    int getSize();
}

// Leafクラス
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;
    }
}

// Compositeクラス
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構造の使用例:

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は、操作を再帰的にその子要素に委譲します。クライアントは、Leafを扱っているのかCompositeを扱っているのかを意識することなく、ツリー構造を操作できます。

ユースケース: 組織図、UIコンポーネント、メニューシステム、ファイルシステムなど、グループと個々の要素を同一に扱う必要があるあらゆる階層構造。

自分で試してみよう

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腕試し

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

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