compositeПаттерн Компоновщик (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);
    }       
}