Menu
Coddy logo textTech

Compositeパターン

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

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

このパターンには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 composite は children を格納し、それらを反復処理することで操作を実装します。folder に対して getSize() を呼び出すと、含まれているすべての項目の合計サイズを再帰的に計算します。クライアントコードは、ファイルまたは folder のどちらを扱っているのかを知る必要がありません。どちらも同じインターフェースに応答します。

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

challenge icon

チャレンジ

簡単

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

コードを3つのファイルに分けて整理します。

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

    抽象 OrgComponent クラスを作成し、以下を含めます。

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

    仮想デストラクタを含めます。

  • Organization.h:リーフクラスとコンポジットクラスを実装します。

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

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

    名前と、std::shared_ptr を使った OrgComponent の子のコレクションを保持する Department クラス(コンポジット)を作成します。以下を実装します。

    • add(std::shared_ptr<OrgComponent> component):部門に子を追加する
    • getSalary():すべてのメンバーの合計給与を再帰的に計算する
    • display():部門名を角括弧で囲んで出力し、その後、すべての子をインデントを増やして表示する(レベルごとに2つのスペースを追加)

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

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

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

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

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

    構造を表示した後、次を出力します。

    Total Salary: $[amount]

たとえば、入力が TechCorpEngineeringAlice,75000Bob,65000 の場合:

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

入力が StartupIncDevelopmentCarol,80000Dave,70000 の場合:

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

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

自分で試してみよう

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

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

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

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