階層構造を扱うコードは、工夫を行わないと煩雑さが膨らんでいきます。
Compositeパターンを使用することで、美しく階層構造を表現することが出来ます。
本記事ではCompositeパターンについて、ファイルとディレクトリの関係を模したコードを例に紹介したいと思います。
前提知識
-
C++の基礎文法を理解している
Compositeパターンとは
Composite(コンポジット)パターンとは、
個々の要素(リーフ)と、それらをまとめた集合(コンポジット)を同じインターフェースで扱えるようにするためのデザインパターンです。
葉とコンポジットの両方が同じインターフェースを実装することで、呼び出し側は構造の違いを意識せずに操作できるようになります。
その反面で、再帰的に処理を委譲するためデバッグが難しくなるといったデメリットも存在します。
Compositeパターンの用語と構成
Compositeパターンは以下の要素で構成されます。
-
Component/コンポーネント
- 共通のインターフェース
-
リーフとコンポジットを同一視するための窓口
-
Leaf/リーフ
- 子を持たない要素
- 実際の処理を行う最小単位
-
Composite/コンポジット
- 子を持つ要素
- 内部に複数の
Componentを保持し、処理を委譲する
構造の図解
サンプルコード
//--------------------------------------------------------------
//! @file Component.h
//! @brief CompositeパターンのComponentインターフェースクラスの定義
//! @author つきの
//--------------------------------------------------------------
#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <vector>
//--------------------------------------------------------------
//! @class Component
//! @brief Compositeパターンのすべての基底クラス
//! @details CompositeパターンのComponentに相当
//--------------------------------------------------------------
class Component {
public:
//--------------------------------------------------------------
//! @brief 仮想デストラクタ
//--------------------------------------------------------------
virtual ~Component() = default;
//--------------------------------------------------------------
//! @brief コンポーネントの情報を表示する純粋仮想関数
//! @param indent インデントの深さ
//--------------------------------------------------------------
virtual void print(int indent = 0) const = 0;
};
//--------------------------------------------------------------
//! @file Directory.h
//! @brief ファイルシステムのディレクトリを表すクラスの定義
//! @author つきの
//--------------------------------------------------------------
#pragma once
#include "Component.h"
//--------------------------------------------------------------
//! @class Directory
//! @brief ファイルシステムのディレクトリを表すクラス
//! @details CompositeパターンのCompositeに相当
//--------------------------------------------------------------
class Directory : public Component {
public:
//--------------------------------------------------------------
// コンストラクタ
//! @param name ディレクトリ名
//--------------------------------------------------------------
explicit Directory(std::string name);
//--------------------------------------------------------------
// ディレクトリにコンポーネントを追加する
//! @param entry 追加するコンポーネント
//--------------------------------------------------------------
void add(std::shared_ptr<Component> entry);
//--------------------------------------------------------------
// コンポーネントの情報を表示する
//! @param indent インデントの深さ
//--------------------------------------------------------------
void print(int indent = 0) const override;
private:
std::string name_; //!< ディレクトリ名
std::vector<std::shared_ptr<Component>> children_; //!< ディレクトリ内のコンポーネント一覧
};
//--------------------------------------------------------------
//! @file Directory.cpp
//! @brief ファイルシステムのディレクトリを表すクラスの実装
//! @author つきの
//--------------------------------------------------------------
#include "Directory.h"
//--------------------------------------------------------------
//! @brief コンストラクタ
//--------------------------------------------------------------
Directory::Directory(std::string name)
: name_(std::move(name)) {
}
//--------------------------------------------------------------
//! @brief ディレクトリにコンポーネントを追加する
//--------------------------------------------------------------
void Directory::add(std::shared_ptr<Component> entry) {
children_.push_back(std::move(entry));
}
//--------------------------------------------------------------
//! @brief コンポーネントの情報を表示する
//--------------------------------------------------------------
void Directory::print(int indent) const {
// ディレクトリ名を表示
std::cout << std::string(indent, ' ') << "[ " << name_ << " ]\n";
// 子コンポーネントを表示
for (const auto& child : children_) {
child->print(indent + 2);
}
}
//--------------------------------------------------------------
//! @file File.h
//! @brief ファイルシステムのファイルを表すクラスの定義
//! @author つきの
//--------------------------------------------------------------
#pragma once
#include "Component.h"
//--------------------------------------------------------------
//! @class File
//! @brief ファイルシステムのファイルを表すクラス
//! @details CompositeパターンのLeafに相当
//--------------------------------------------------------------
class File : public Component {
public:
//--------------------------------------------------------------
// コンストラクタ
//! @param name ファイル名
//--------------------------------------------------------------
explicit File(std::string name);
//--------------------------------------------------------------
//! @brief コンポーネントの情報を表示する
//! @param indent インデントの深さ
//--------------------------------------------------------------
void print(int indent = 0) const override;
private:
std::string name_; //!< ファイル名
};
//--------------------------------------------------------------
//! @file File.cpp
//! @brief ファイルシステムのファイルを表すクラスの宣言
//! @author つきの
//--------------------------------------------------------------
#include "File.h"
//--------------------------------------------------------------
//! @brief コンストラクタ
//--------------------------------------------------------------
File::File(std::string name)
: name_(std::move(name)) {
}
//--------------------------------------------------------------
//! @brief コンポーネントの情報を表示する
//--------------------------------------------------------------
void File::print(int indent) const {
std::cout << std::string(indent, ' ') << "- " << name_ << "\n";
}
//--------------------------------------------------------------
//! @file main.h
//! @brief Compositeパターンのサンプルコード
//! @author つきの
//--------------------------------------------------------------
#include "Directory.h"
#include "File.h"
// エントリポイント
int main() {
//--------------------------------------------------------------
// ルートにディレクトリを作る
//--------------------------------------------------------------
auto root = std::make_shared<Directory>("root"); // ルートディレクトリ
//--------------------------------------------------------------
// rootにhomeディレクトリを追加する
//--------------------------------------------------------------
auto home = std::make_shared<Directory>("home"); // homeディレクトリを作って
root->add(home); // rootに追加
//--------------------------------------------------------------
// homeにファイルを追加する
//--------------------------------------------------------------
auto test_file = std::make_shared<File>("test.txt"); // test.txtファイルを作って
home->add(test_file); // homeに追加
// 階層構造の表示
root->print();
// プログラム終了
return 0;
}
[ root ]
[ home ]
- test.txt
階層構造が表現できました。
クラス図
Component/コンポーネント
Componentは、ファイルとディレクトリ両方の共通インターフェースです。
これを継承することで、Compositeから階層に追加することが可能になります。
//--------------------------------------------------------------
//! @class Component
//! @brief Compositeパターンのすべての基底クラス
//! @details CompositeパターンのComponentに相当
//--------------------------------------------------------------
class Component {
public:
//--------------------------------------------------------------
//! @brief 仮想デストラクタ
//--------------------------------------------------------------
virtual ~Component() = default;
//--------------------------------------------------------------
//! @brief コンポーネントの情報を表示する純粋仮想関数
//! @param indent インデントの深さ
//--------------------------------------------------------------
virtual void print(int indent = 0) const = 0;
};
Composite/コンポジット
階層構造の子として、Componentを継承したオブジェクトを追加できる関数を作成します。リーフ(本記事ではFile)とコンポジット(本記事ではDirectory)が共にComponentを継承しているため、どちらも追加することができます。
//--------------------------------------------------------------
// ディレクトリにコンポーネントを追加する
//! @param entry 追加するコンポーネント
//--------------------------------------------------------------
void add(std::shared_ptr<Component> entry);
//--------------------------------------------------------------
//! @brief ディレクトリにコンポーネントを追加する
//--------------------------------------------------------------
void Directory::add(std::shared_ptr<Component> entry) {
children_.push_back(std::move(entry));
}
Leaf/リーフ
Leafは、子を持たない要素で、階層構造ではくっつけられることだけができるオブジェクトです。
本記事で情報表示とコンストラクタのみを実装しています。
//--------------------------------------------------------------
//! @class File
//! @brief ファイルシステムのファイルを表すクラス
//! @details CompositeパターンのLeafに相当
//--------------------------------------------------------------
class File : public Component {
public:
//--------------------------------------------------------------
// コンストラクタ
//! @param name ファイル名
//--------------------------------------------------------------
explicit File(std::string name);
//--------------------------------------------------------------
//! @brief コンポーネントの情報を表示する
//! @param indent インデントの深さ
//--------------------------------------------------------------
void print(int indent = 0) const override;
private:
std::string name_; //!< ファイル名
};
//--------------------------------------------------------------
//! @brief コンストラクタ
//--------------------------------------------------------------
File::File(std::string name)
: name_(std::move(name)) {
}
//--------------------------------------------------------------
//! @brief コンポーネントの情報を表示する
//--------------------------------------------------------------
void File::print(int indent) const {
std::cout << std::string(indent, ' ') << "- " << name_ << "\n";
}
総括
-
Compositeパターンを使用することで、階層構造を美しく管理することができる - 再帰的な管理のため、デバッグが難しくなってしまうというデメリットも存在する
-
Component(共通インターフェース)を継承することで、アップキャストによってCompositeがCompositeとLeafを同じように管理することができる