Menu
Coddy logo textTech

コンポジットパターン

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

Composite パターンを使用すると、個々のオブジェクトとオブジェクトのグループを一貫して扱うことができます。これは、単一の要素と要素のコンテナの両方が同じインターフェースを共有するツリー構造にオブジェクトを構成します。これは、ファイルシステム、組織図、UIコンポーネントなどの階層構造を表現するのに最適です。

このパターンには3つの主要な部分があります:共通の操作を定義するComponentインターフェース、個々のオブジェクトを表すLeafクラス、そして子を保持し、それらに操作を委譲するCompositeクラスです:

#include <iostream>
#include <memory>
#include <vector>
#include <string>

// コンポーネントインターフェース
class FileSystemItem {
public:
    virtual void display(int indent = 0) const = 0;
    virtual int getSize() const = 0;
    virtual ~FileSystemItem() = default;
};

// Leaf(葉) - 個々のファイルを表します
class File : public FileSystemItem {
    std::string name;
    int size;
public:
    File(const std::string& n, int s) : name(n), size(s) {}
    
    void display(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << name 
                  << " (" << size << " KB)\n";
    }
    int getSize() const override { return size; }
};

// Composite(複合体) - 他のコンポーネントを含みます
class Folder : public FileSystemItem {
    std::string name;
    std::vector<std::shared_ptr<FileSystemItem>> children;
public:
    Folder(const std::string& n) : name(n) {}
    
    void add(std::shared_ptr<FileSystemItem> item) {
        children.push_back(item);
    }
    
    void display(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << "[" << name << "]\n";
        for (const auto& child : children) {
            child->display(indent + 2);
        }
    }
    
    int getSize() const override {
        int total = 0;
        for (const auto& child : children) {
            total += child->getSize();
        }
        return total;
    }
};

Folder コンポジットは子要素を保持し、それらを反復処理することで操作を実装します。フォルダーに対して getSize() を呼び出すと、含まれるすべてのアイテムの合計サイズを再帰的に計算します。クライアントコードは、ファイルとフォルダーのどちらを操作しているかを意識する必要はありません。どちらも同じインターフェースに応答するためです。

部分-全体階層を表現する必要があり、クライアントが個々のオブジェクトと合成物を一律に扱えるようにしたい場合に、Compositeを使用します。

challenge icon

チャレンジ

簡単

Composite パターンを使用して、組織図システムを構築しましょう。個々の従業員と、他の従業員やサブ部門を含む部門の両方を一律に扱える階層構造を作成します。これは、部門が人と他の部門を含み、ツリー構造を形成するという実際の企業の構造を反映しています。

コードは以下の3つのファイルに分けて構成します。

  • OrgComponent.h: 従業員と部門の両方が実装するコンポーネントインターフェースを定義します。

    以下のメソッドを持つ抽象クラス OrgComponent を作成してください:

    • getName() — コンポーネントの名前を返します
    • getSalary() — 合計給与を返します(従業員の場合は自身の給与、部門の場合は含まれるすべての給与の合計)
    • display(int indent = 0) — 適切なインデントでコンポーネントを表示します

    仮想デストラクタを含めてください。

  • Organization.h: Leaf(末端)クラスと Composite(複合)クラスを実装します。

    名前と給与を保持する Employee クラス(Leaf)を作成します。その display() メソッドは、従業員情報を次の形式で出力する必要があります:

    [indent spaces]- [name] ($[salary])

    名前と std::shared_ptr を使用した OrgComponent の子要素のコレクションを保持する Department クラス(Composite)を作成します。以下を実装してください:

    • add(std::shared_ptr<OrgComponent> component) — 部門に子要素を追加します
    • getSalary() — すべてのメンバーの合計給与を再帰的に計算します
    • display() — 部門名をブラケットで囲んで出力し、次にインデントを増やして(レベルごとに2スペース追加)すべての子要素を表示します

    部門の表示形式は次のようになります:

    [indent spaces][Department Name]
      [children displayed with indent + 2]
  • main.cpp: 組織構造を構築し、表示します。

    4つの入力を読み取ります:

    1. 会社名 (文字列)
    2. 部門名 (文字列)
    3. 1人目の従業員の名前と給与 (形式: name,salary)
    4. 2人目の従業員の名前と給与 (形式: name,salary)

    次の構造を構築してください:会社(トップレベルの部門)を作成し、そこにサブ部門を追加し、そのサブ部門に両方の従業員を追加します。その後、組織全体を表示し、会社の総給与を出力します。

    構造を表示した後、次のように出力してください:

    Total Salary: $[amount]

例えば、入力が TechCorp, Engineering, Alice,75000, Bob,65000 の場合:

[TechCorp]
  [Engineering]
    - Alice ($75000)
    - Bob ($65000)
Total Salary: $140000

入力が StartupInc, Development, Carol,80000, Dave,70000 の場合:

[StartupInc]
  [Development]
    - Carol ($80000)
    - Dave ($70000)
Total Salary: $150000

getSalary() が、従業員に対して呼び出されたか部門に対して呼び出されたかに関わらず、一律に機能することに注目してください。部門は自動的にすべてのメンバーの給与を集計します。クライアントコードは、合計を計算したり階層を表示したりする際に、個々の従業員と部門全体を区別する必要はありません。

チートシート

Composite パターンは、個々のオブジェクトとオブジェクトのグループをツリー構造に構成することで、単一の要素とコンテナの両方が同じインターフェースを共有し、それらを一貫して扱うことを可能にします。

このパターンには、3つの主要な構成要素があります。

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

ファイルシステムの実装例:

#include <iostream>
#include <memory>
#include <vector>
#include <string>

// Component インターフェース
class FileSystemItem {
public:
    virtual void display(int indent = 0) const = 0;
    virtual int getSize() const = 0;
    virtual ~FileSystemItem() = default;
};

// Leaf - 個々のファイルを表す
class File : public FileSystemItem {
    std::string name;
    int size;
public:
    File(const std::string& n, int s) : name(n), size(s) {}
    
    void display(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << name 
                  << " (" << size << " KB)\n";
    }
    int getSize() const override { return size; }
};

// Composite - 他のコンポーネントを含む
class Folder : public FileSystemItem {
    std::string name;
    std::vector<std::shared_ptr<FileSystemItem>> children;
public:
    Folder(const std::string& n) : name(n) {}
    
    void add(std::shared_ptr<FileSystemItem> item) {
        children.push_back(item);
    }
    
    void display(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << "[" << name << "]\n";
        for (const auto& child : children) {
            child->display(indent + 2);
        }
    }
    
    int getSize() const override {
        int total = 0;
        for (const auto& child : children) {
            total += child->getSize();
        }
        return total;
    }
};

Composite は std::shared_ptr を使用して子要素を格納し、それらを反復処理することで操作を実装します。getSize() のような操作は再帰的に動作し、階層全体で合計を計算します。

部分-全体階層を表現する必要があり、クライアントが個々のオブジェクトと合成オブジェクトを一貫して扱えるようにしたい場合に、Composite パターンを使用します。

自分で試してみよう

#include <iostream>
#include <string>
#include <memory>
#include <sstream>
#include "Organization.h"

int main() {
    // 入力を読み込む
    std::string companyName;
    std::string departmentName;
    std::string employee1Input;
    std::string employee2Input;
    
    std::getline(std::cin, companyName);
    std::getline(std::cin, departmentName);
    std::getline(std::cin, employee1Input);
    std::getline(std::cin, employee2Input);
    
    // "name,salary" 形式をパースするためのヘルパーラムダ
    auto parseEmployee = [](const std::string& input) -> std::pair<std::string, int> {
        size_t commaPos = input.find(',');
        std::string name = input.substr(0, commaPos);
        int salary = std::stoi(input.substr(commaPos + 1));
        return {name, salary};
    };
    
    auto [name1, salary1] = parseEmployee(employee1Input);
    auto [name2, salary2] = parseEmployee(employee2Input);
    
    // TODO: 会社をトップレベルの Department として作成する
    
    // TODO: サブデパートメント(部署)を作成する
    
    // TODO: パースしたデータを使用して2つの Employee オブジェクトを作成する
    
    // TODO: 従業員をサブデパートメントに追加する
    
    // TODO: サブデパートメントを会社に追加する
    
    // TODO: 組織構造全体を表示する
    
    // TODO: 合計給与を次の形式で出力する: Total Salary: $[amount]
    
    return 0;
}
quiz icon腕試し

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

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