C#のコンポジットパターン完全攻略!木構造データを管理する設計術
生徒
「C#でフォルダーの中にフォルダーがあるような、木のような構造データを扱うにはどうすればいいですか?」
先生
「C#のデザインパターンの一つである、コンポジットパターンを使うと非常にスッキリと管理できますよ。」
生徒
「コンポジットパターンって何ですか?難しそうですが、どういう仕組みか教えてください。」
先生
「大丈夫です。整理整頓の例えを使って、基本から一緒に見ていきましょう。」
1. コンポジットパターンとは何か
コンポジットパターンとは、一言でいえば「入れ物」と「中身」を同じ種類のものとして扱うための設計テクニックです。プログラミングの現場では、デザインパターンという、先人が見つけた「使いやすい設計の型」の一つとして知られています。このパターンを使うと、複雑な構造であっても、まるで単純な一つのアイテムであるかのように操作できるのが特徴です。
例えば、パソコンのフォルダーをイメージしてください。フォルダーの中には、ファイルが入っていることもあれば、さらに別のフォルダーが入っていることもありますよね。この「フォルダーの中にフォルダーがある」という構造を木構造と呼びます。コンポジットパターンを使うと、フォルダーもファイルも同じ「中身」として統一して扱うことができるため、プログラムが驚くほどシンプルになります。
2. 木構造とコンポジットの基礎知識
まず、なぜ木構造という言葉を使うのかを解説します。プログラムの世界では、一番上の階層(ルート)から枝分かれしていくデータ構造を、逆さまの木に見立ててツリー構造や木構造と呼びます。ここで出てくる基本的な用語を整理しましょう。
- リーフ(葉):それ以上中身を持たない、一番端にある要素のこと。今回の例では「ファイル」に当たります。
- コンポジット(合成物):中に他の要素を保持できる入れ物のこと。今回の例では「フォルダー」に当たります。
コンポジットパターンの肝は、これら「リーフ」と「コンポジット」に共通の役割を与えることです。これにより、プログラムを書くとき、それがファイルなのかフォルダーなのかを逐一気にすることなく、共通の命令を送ることができるようになります。
3. インターフェースで共通のルールを作る
C#でコンポジットパターンを実装する際、まず必要なのがインターフェースです。インターフェースとは、プログラムの中で「この機能は必ず持っていなければならない」というルールを定義する設計図のようなものです。今回は「中身を表示する」というルールを定義してみましょう。
public interface IFileSystemItem
{
void Display(string indent);
}
ここでは、IFileSystemItemという名前のインターフェースを作りました。これによって、ファイルクラスもフォルダークラスも、このインターフェースに従うことで、「表示する」という機能を持たなければならないというルールが決まりました。
4. リーフとなるファイルクラスを作成する
次に、一番下の単位であるファイルクラスを作成します。これは単純に名前を表示するだけのクラスです。プログラム未経験の方も、以下のコードを見ると「名前を表示しているだけだな」とイメージできるはずです。
public class File : IFileSystemItem
{
private string name;
public File(string name) { this.name = name; }
public void Display(string indent)
{
Console.WriteLine(indent + "- ファイル: " + name);
}
}
このFileクラスは、Displayという命令を受けると、自分の名前を表示します。この時点では非常に単純な構造ですね。これが後ほど、フォルダーという「入れ物」の中にたくさん格納されることになります。
5. コンポジットとなるフォルダークラスを作成する
ここが今回の主役、フォルダークラスです。Listという、データを複数保管するための箱を使って、ファイルやさらに別のフォルダーを管理します。
using System.Collections.Generic;
public class Folder : IFileSystemItem
{
private string name;
private List<IFileSystemItem> children = new List<IFileSystemItem>();
public Folder(string name) { this.name = name; }
public void Add(IFileSystemItem item) { children.Add(item); }
public void Display(string indent)
{
Console.WriteLine(indent + "+ フォルダー: " + name);
foreach (var item in children)
{
item.Display(indent + " ");
}
}
}
このクラスの面白いところは、Displayの中で自分の中身に対して再度Displayを呼び出している点です。これを再帰的処理と呼びます。自分の中に何が入っていても、その中身にも「表示せよ」と命令をリレーしていくことで、木構造全体を自動的に表示できるのです。
6. 実際に木構造を動かしてみる
最後に、これまで作ったクラスを組み合わせて、実際に木構造を作ってみましょう。Mainメソッドという、プログラムが最初に動き出す場所で、フォルダーを作り、その中にファイルやフォルダーを追加していきます。
class Program
{
static void Main()
{
Folder root = new Folder("ルート");
Folder docs = new Folder("ドキュメント");
docs.Add(new File("メモ.txt"));
root.Add(docs);
root.Add(new File("設定.ini"));
root.Display("");
}
}
このコードを実行すると、以下のようにきれいな階層構造が表示されます。
+ フォルダー: ルート
+ フォルダー: ドキュメント
- ファイル: メモ.txt
- ファイル: 設定.ini
このように、コンポジットパターンを使うと、階層の深さを気にすることなく、スマートに木構造データを操作できるようになります。初心者の方でも、この「共通ルールを作って再帰的に呼び出す」という仕組みをマスターすれば、より高度なアプリケーション開発がぐっと身近になるはずです。