組合模式將物件組合成樹狀結構以表示「部分-整體」層次,讓客戶端以統一介面處理單一物件與組合物件。本文以「檔案系統(資料夾/檔案)」為經典場景,展示 C# 實作要點,並探討如何避免常見陷阱。
Add/Remove/DisplayAdd/Remove 拋異常或無視Display
using System;
using System.Collections;
public abstract class FileSystemElement
{
protected string _name;
protected FileSystemElement(string name) => _name = name;
public abstract void Add(FileSystemElement element);
public abstract void Remove(FileSystemElement element);
public abstract void Display(int depth = 0);
}
public class FileElement : FileSystemElement
{
public FileElement(string name) : base(name) { }
public override void Add(FileSystemElement element)
=> Console.WriteLine($"Cannot add to a file: {_name}");
public override void Remove(FileSystemElement element)
=> Console.WriteLine($"Cannot remove from a file: {_name}");
public override void Display(int depth = 0)
=> Console.WriteLine($"{new string(' ', depth * 2)}📄 {_name}");
}
public class FolderElement : FileSystemElement
{
private readonly ArrayList _children = new ArrayList();
public FolderElement(string name) : base(name) { }
public override void Add(FileSystemElement element)
=> _children.Add(element);
public override void Remove(FileSystemElement element)
=> _children.Remove(element);
public override void Display(int depth = 0)
{
Console.WriteLine($"{new string(' ', depth * 2)}📁 {_name}");
foreach (FileSystemElement child in _children)
child.Display(depth + 1);
}
}
class Program
{
static void Main()
{
var root = new FolderElement("Z:/");
var games = new FolderElement("Games/");
games.Add(new FileElement("cstrike.exe"));
games.Add(new FileElement("hl2.exe"));
root.Add(games);
var docs = new FolderElement("Documents/");
docs.Add(new FileElement("resume.pdf"));
root.Add(docs);
root.Display();
Console.ReadKey();
}
}
📁 Z:/
📁 Games/
📄 cstrike.exe
📄 hl2.exe
📁 Documents/
📄 resume.pdf
IComposite 定義 Add/Remove,IFileSystemElement 僅定義 Display,Leaf 不實作 IComposite| 集合類型 | 優點 | 缺點 |
|---|---|---|
| ArrayList / List<T> | 簡單、隨機存取 | 頻繁插刪效率低 |
| LinkedList<T> | 插刪 O(1) | 無隨機存取、記憶體開銷大 |
| Dictionary<string, T> | 按名稱快速查找 | 順序不穩定、鍵不可重複 |
一般場景 List<FileSystemElement> 足夠;需高效查找時改 Dictionary。
若需「從檔案找資料夾路徑」,在 Add 時設定 element.Parent = this,但要小心循環引用與序列化問題。
public interface ITreeIterator
{
bool MoveNext();
FileSystemElement Current { get; }
}
public class DepthFirstIterator : ITreeIterator { ... }
public class BreadthFirstIterator : ITreeIterator { ... }
將遍歷策略外部化,符合單一職責原則。
public interface IFileSystemNode
{
string Name { get; }
void Display(string indent = "");
}
public interface IContainer : IFileSystemNode
{
void Add(IFileSystemNode node);
void Remove(IFileSystemNode node);
IReadOnlyList Children { get; }
}
public class FileNode : IFileSystemNode
{
public string Name { get; }
public FileNode(string name) => Name = name;
public void Display(string indent = "") => Console.WriteLine($"{indent}📄 {Name}");
}
public class FolderNode : IContainer
{
public string Name { get; }
private readonly List _children = new();
public FolderNode(string name) => Name = name;
public void Add(IFileSystemNode node) => _children.Add(node);
public void Remove(IFileSystemNode node) => _children.Remove(node);
public IReadOnlyList Children => _children.AsReadOnly();
public void Display(string indent = "")
{
Console.WriteLine($"{indent}📁 {Name}");
foreach (var child in _children) child.Display(indent + " ");
}
}
結語:組合模式以「遞歸結構 + 統一介面」優雅解決樹狀結構操作難題。現代 C# 配合介面隔離與泛型,能將 GoF 原版的「Leaf 拋異常」缺陷徹底修正。下次遇到樹狀結構,先問自己:這裡有部分-整體關係嗎?客戶端需要統一介面嗎? 答案若是肯定,組合模式就是首選。