Паттерн Компоновщик (Composite) объединяет группы объектов в древовидную структуру по принципу “часть-целое и позволяет клиенту одинаково работать как с отдельными объектами, так и с группой объектов.
Образно реализацию паттерна можно представить в виде меню, которое имеет различные пункты. Эти пункты могут содержать подменю, в которых, в свою очередь, также имеются пункты. То есть пункт меню служит с одной стороны частью меню, а с другой стороны еще одним меню. В итоге мы однообразно можем работать как с пунктом меню, так и со всем меню в целом.
Когда использовать компоновщик?
- Когда объекты должны быть реализованы в виде иерархической древовидной структуры
- Когда клиенты единообразно должны управлять как целыми объектами, так и их составными частями. То есть целое и его части должны реализовать один и тот же интерфейс
Формальное определение паттерна на C# могло бы выглядеть так:
class Client
{
public void Main()
{
Component root = new Composite("Root");
Component leaf = new Leaf("Leaf");
Composite subtree = new Composite("Subtree");
root.Add(leaf);
root.Add(subtree);
root.Display();
}
}
abstract class Component
{
protected string name;
public Component(string name)
{
this.name = name;
}
public abstract void Display();
public abstract void Add(Component c);
public abstract void Remove(Component c);
}
class Composite : Component
{
List<Component> children = new List<Component>();
public Composite(string name)
: base(name)
{}
public override void Add(Component component)
{
children.Add(component);
}
public override void Remove(Component component)
{
children.Remove(component);
}
public override void Display()
{
Console.WriteLine(name);
foreach (Component component in children)
{
component.Display();
}
}
}
class Leaf : Component
{
public Leaf(string name)
: base(name)
{}
public override void Display()
{
Console.WriteLine(name);
}
public override void Add(Component component)
{
throw new NotImplementedException();
}
public override void Remove(Component component)
{
throw new NotImplementedException();
}
}
Участники
- Component: определяет интерфейс для всех компонентов в древовидной структуре
- Composite: представляет компонент, который может содержать другие компоненты и реализует механизм для их добавления и удаления
- Leaf: представляет отдельный компонент, который не может содержать другие компоненты
- Client: клиент, который использует компоненты
Рассмотрим простейший пример. Допустим, нам надо создать объект файловой системы. Файловую систему составляют папки и файлы. Каждая папка также может включать в себя папки и файлы. То есть получается древовидная иерархическая структура, где с вложенными папками нам надо работать также, как и с папками, которые их содержат. Для реализации данной задачи и воспользуемся паттерном Компоновщик:
class Program
{
static void Main(string[] args)
{
Component fileSystem = new Directory("Файловая система");
// определяем новый диск
Component diskC = new Directory("Диск С");
// новые файлы
Component pngFile = new File("12345.png");
Component docxFile = new File("Document.docx");
// добавляем файлы на диск С
diskC.Add(pngFile);
diskC.Add(docxFile);
// добавляем диск С в файловую систему
fileSystem.Add(diskC);
// выводим все данные
fileSystem.Print();
Console.WriteLine();
// удаляем с диска С файл
diskC.Remove(pngFile);
// создаем новую папку
Component docsFolder = new Directory("Мои Документы");
// добавляем в нее файлы
Component txtFile = new File("readme.txt");
Component csFile = new File("Program.cs");
docsFolder.Add(txtFile);
docsFolder.Add(csFile);
diskC.Add(docsFolder);
fileSystem.Print();
Console.Read();
}
}
abstract class Component
{
protected string name;
public Component(string name)
{
this.name = name;
}
public virtual void Add(Component component){}
public virtual void Remove(Component component) { }
public virtual void Print()
{
Console.WriteLine(name);
}
}
class Directory :Component
{
private List<Component> components = new List<Component>();
public Directory(string name)
: base(name)
{
}
public override void Add(Component component)
{
components.Add(component);
}
public override void Remove(Component component)
{
components.Remove(component);
}
public override void Print()
{
Console.WriteLine("Узел " + name);
Console.WriteLine("Подузлы:");
for(int i=0; i<components.Count;i++)
{
components[i].Print();
}
}
}
class File : Component
{
public File(string name)
: base(name)
{}
}
В итоге подобная система обладает неплохой гибкостью: если мы захотим добавить новый вид компонентов, нам достаточно унаследовать новый класс от Component.
И также применяя компоновщик, мы легко можем обойти все узлы древовидной структуры.
Реализация шаблона в общем виде
- определяем необходимый набор методов и свойств, которые составят общий интерфейс IComponent;
- специфику составного объекта описываем в IComposite;
- разрабатываем базовую реализацию Component и ее уточнение Composite;
- в процессе работы приложения клиенту для использования передается общий IComponent.
Пример реализации
Для примера напишем код вывода карты города. Разумеется, упростим его и отбросим все, что не требуется для понимания реализации шаблона.
Начнем разработку с общего интерфейса, который включает ссылку на родителя, имя, а так же методы для отображения на карте и поиска потомка по имени. Будем считать, что компонент хранит положение относительно своего родительского элемента, координаты левого нижнего угла которого передаются ему в параметрах x и y метода Draw().
public interface IMapComponent
{
IMapComponent Parent { get; set; }
string Title { get; set; }
void Draw(int x, int y);
IMapComponent FindChild(string name);
}
Создадим базовую реализацию данного интерфейса. Поскольку в общем случае не известно как выводить компонент, то сделаем метод Draw() абстрактным. Добавим два поля для сохранения относительных координат объекта: _x и _y. Кроме того, в общем случае нет данных о потомках, поэтому FindChild() будет просто проверять соответствие имени данного экземпляра искомому.
public abstract class MapComponent : IMapComponent
{
protected int _x;
protected int _y;
public IMapComponent Parent { get; set; }
public string Title { get; set; }
public abstract void Draw(int x, int y);
public virtual IMapComponent FindChild(string name)
{
return (this.Title == name) ? this : null;
}
}
Перейдем к составному объекту. Для демонстрации потребуется только метод добавления элемента. Поэтому уточнение интерфейса будет очень кратким:
public interface IMapComposite : IMapComponent
{
void AddComponent(IMapComponent component);
}
В качестве базы для реализации составного объекта будем использовать MapComponent. В этом случае необходимо только переопределить методы Draw() и FindChild(), а так же реализовать AddComponent(). С последним все просто: будем использовать List в роли хранилища объектов и просто добавим новый элемент в его список.
Как было уже отмечено, составные объекты для своих операций используют методы своих частей. Поэтому реализация Draw() и Find() так же является достаточно очевидной.
public class MapComposite : MapComponent, IMapComposite
{
private List<IMapComponent> _components = new List<IMapComponent>();
public void AddComponent(IMapComponent component)
{
this._components.Add(component);
component.Parent = this;
}
public override void Draw(int x, int y)
{
Console.WriteLine(this.Title);
foreach (IMapComponent component in this._components) {
component.Draw(this._x + x, this._y + y);
}
}
public override IMapComponent FindChild(string name)
{
if (this.Title == name) {
return this;
}
foreach (IMapComponent component in this._components) {
IMapComponent found = component.FindChild(name);
if (found != null) {
return found;
}
}
return null;
}
}
Осталось определить объекты, представляющие части. Они будут являться потомками MapComponent с переопределенным методом Draw().
public class MapHouse : MapComponent
{
public override void Draw(int x, int y)
{
Console.WriteLine("{0} House", this.Title);
}
}
public class MapRoad : MapComponent
{
public override void Draw(int x, int y)
{
Console.WriteLine("{0} Road", this.Title);
}
}
public class MapLeftTurn : MapComponent
{
public override void Draw(int x, int y)
{
Console.WriteLine("{0} Left turn", this.Title);
}
}
public class MapRightTurn : MapComponent
{
public override void Draw(int x, int y)
{
Console.WriteLine("{0} Right turn", this.Title);
}
}
На этом реализация шаблона Компоновщик завершена и прейдём к примеру использования.
Как правило, в проектах древообразные структуры создаются из различных источников данных с использованием Строителя и Фабричного метода. Но, чтобы не усложнять пример, породим все элементы самостоятельно в методе BuildCity().
Обратите внимание, что клиент в своих методах DrawArea() и Execute() не делает различий между типами объектов.
public static class Demo
{
public static IMapComponent BuildCity()
{
IMapComposite road1 = new MapComposite() { Title = "Main Street" };
road1.AddComponent(new MapRoad() { Title = road1.Title });
road1.AddComponent(new MapRoad() { Title = road1.Title });
road1.AddComponent(new MapLeftTurn() { Title = road1.Title });
road1.AddComponent(new MapRoad() { Title = road1.Title });
road1.AddComponent(new MapRightTurn() { Title = road1.Title });
IMapComposite district1 = new MapComposite() { Title = "District 1" };
district1.AddComponent(new MapHouse() { Title = "House 1" });
district1.AddComponent(new MapHouse() { Title = "House 2" });
district1.AddComponent(new MapHouse() { Title = "House 3" });
district1.AddComponent(road1);
IMapComposite city = new MapComposite() { Title = "New city" };
city.AddComponent(district1);
return city;
}
public static void DrawArea(IMapComponent component)
{
if (component == null) {
return;
}
Console.WriteLine("Drawing ...");
component.Draw(0,0);
Console.WriteLine("==============n");
}
public static void Execute()
{
IMapComponent city = BuildCity();
DrawArea(city);
IMapComponent road = city.FindChild("Main Street");
DrawArea(road);
IMapComponent house = city.FindChild("House 2");
DrawArea(house);
}
}