Структурные шаблоны: Компоновщик (Composite)

Название шаблона

Компоновщик (Composite).

Тип

Структурный шаблон проектирования (Structural).

Описание

Сomposite patternКомпоновщик позволяет упростить и стандартизировать взаимодействие между клиентом и группой объектов, представляющих древовидную структуру "составной объект – его части".

Данный шаблон используется если необходимо:

  • представить группу объектов в виде "составной объект – его части";
  • (и) чтобы клиенты одинаково обращались как к составным объектам, так и к отдельным частям.

Для того, чтобы понять суть Компоновщика рассмотрим следующий пример: предположим, необходимо вывести на экран карту города или ее область. При этом город можно представить как группу районов, каждый район – как группу кварталов, которые в свою очередь состоят из домов, элементов дорог и т.д.

Неделимые, с точки зрения логики приложения, объекты (дома, деревья, прямой участок дороги, повороты) и будут частями, которые в разных комбинациях могут образовывать различные составные объекты (улицы, кварталы, районы, город).

Пусть все классы, представляющие объекты этой древовидной структуры, поддерживают общий интерфейс. Тогда клиент сможет однообразно обращаться как к отдельному "дому", так и к "городу" в целом. При этом, составные объекты, будут переадресовывать запросы всем своим частям. Например, вызов метода отображения района приведет к отображению входящих в него домов, улиц и т.д.

Данный подход применим в различных областях. Например:

  • себестоимость автомобиля можно рассчитать как сумму себестоимостей составляющих его агрегатов и трудозатрат, которые в свою очередь так же могут быть разделены на части;
  • структуру сайта можно представить в виде дерева, где частями будут страницы с контентом, составляющие разделы и каталоги;
  • при расчете статистики по организации, сотрудники являются "частями" участков, которые входят в отделы, входящие в управления и т. д.

Рассмотрим участников шаблона Компоновщик:

  1. Компонент (Component / IComponent) – определяет интерфейс, общий для составных объектов и и частей. Может предоставлять реализацию "по-умолчанию" для стандартных методов.
  2. Составной объект (Composite) – объект, включающий в себя "части".
  3. Часть или Лист (Leaf) – "неделимые" объекты (название "лист" взято по аналогии с наименованием элемента древовидной структуры).

Стоит еще раз отметить, что с точки зрения клиента все объекты являются экземплярами типа Component. Он не различает (и в большинстве случаев не должен различать) составные объекты и их части.

Результатом использования данного шаблона является:

  • упрощение архитектуры клиента, т.к. от него скрыты детали реализации составных объектов и он имеет возможность работать со всеми экземплярами одинаково;
  • возможность легко добавлять новые типы как составных объектов, так и их частей. При этом нет необходимости изменять код клиента.

Особенности применения шаблона

При использовании шаблона Компоновщик можно столкнуться с ситуацией, когда у одной части может быть несколько родительских элементов. Например, для карты можно обойтись одним экземп��яром "дерево" для всех "кварталов". В этом случае можно дополнительно применить шаблон Приспособленец.

Во многих случаях результат работы метода составного объекта напрямую зависит от порядка вызовов входящих в него частей. В таких случаях необходимо учитывать порядок при разработке методов добавления и удаления частей. Это может быть как сортировка, так и методы для обхода коллекции в правильной последовательности (использование шаблона Итератор).

Самый верхний объект древовидной структуры можно рассматривать как сложный. Поэтому для его создания возможно использование шаблона Строитель. Кроме того, для создания частей может быть применен Фабричный метод.

Проектирование интерфейса

Важным моментом является проектирование интерфейса IComponent. Он должен включать в себя как методы составных объектов, так и их частей. Поэтому важно не дать ему превратиться в "божественный" (т.е. собирающий все методы подряд). Для этого необходимо отметить следующее:

  • Методы и свойства для операций с частями не могут поддерживаться самими частями по определению. Добавление их в общий интерфейс только утяжеляет его.
  • Часть приложения все равно будет стараться различить типы объектов, как, например, при добавлении новой части. В этом случае нет принципиальной разницы между приведением типа и проверкой результата вызова определённого метода для определения возможностей объекта. Последнее так же раскрывает его тип (составной или часть) и позволяет использовать специфические методы и свойства.
  • Оставшаяся часть приложения, использует объекты "вслепую". Именно её и можно считать тем клиентом, на основе требований которого можно разрабатывать IComponent.

В результате, можно выделить общий интерфейс и его уточнение для управления потомками:

  • IComponent – содержит только общие методы и свойства, не отражающие специфики различных типов объектов, входящих в реализацию Компоновщика.
  • IComposite – является спецификацией составного объекта. В частности, включает хранилище потомков и методы управления ими (обход, добавление, удаление, поиск).

Исходя из этого, возможно до двух базовых классов: Component и Composite.

Теоретически, возможно ситуация, когда потребуется отдельный интерфейс только для частей (ILeaf). Однако, это уже стоит рассматривать как признак необходимости пересмотра схемы применения шаблона, его интерфейсов и входящих в его состав объектов.

Может показаться что такой подход противоречит принципу шаблона. Но это не так. Например, в примере с картой основная задача клиента – отображение. В этом случае метод общего интерфейса Draw() будет обеспечивать однообразное использование объектов. Уточняющие интерфейсы потребуются при создании самой карты. Однако, в этот момент для порождающего метода типы и так будут раскрыты.

Структурные шаблоны и их отличия

Компоновщик Предоставляет единый интерфейс для взаимодействия с составными объектами и их частями. Упрощает работу клиента, позволяет легко добавлять новые варианты составных объектов и их частей. Включается в виде интерфейса в составные объекты и их части.
Адаптер Изменяет интерфейс объекта не изменяя его функциональности. Может адаптировать несколько объектов к одному интерфейсу. Позволяет повторно использовать уже существующий код. Содержит или наследует адаптируемый объект.
Фасад Объединяет группу объектов под одним специализированным интерфейсом. Упрощает работу с группой объектов, вносит новый уровень абстракции. Содержит или ссылается на объекты, необходимые для реализации специализированного интерфейса.
Мост Разделяет объект на абстракцию и реализацию. Используется для иерархии объектов. Позволяет отдельно изменять (наследовать) абстракцию и реализацию, повышая гибкость системы. Содержит объект(реализацию), который предоставляет методы для заданной абстракций и ее уточнений (наследников).
Декоратор Расширяет возможности объекта, изменяет его поведение. Поддерживает интерфейс декорируемого объекта, но может добавлять новые методы и свойства. Дает возможность динамически менять функциональность объекта. Является альтернативой наследованию (в том числе множественному). Содержит декорируемый объект. Возможна цепочка объектов, вызываемых последовательно.
Прокси Прозрачно замещает объект и управляет доступом к нему. Не изменяет интерфейс или поведение. Упрощает и оптимизирует работу с объектом. Может добавлять свою функциональность, скрывая ее от клиента. Содержит объект или ссылку на него, может управлять существованием замещенного объекта.
Приспособленец

Не ставит целью изменение интерфейса объекта. Но это может потребоваться для получения обратно данных из вынесенной части состояния.

Позволяет уменьшить число экземпляров объекта в приложении и тем самым сэкономить его ресурсы. Выносит контекстно-зависимую часть состояния объекта вовне, заменяя несколько его экземпляров одним.

Реализация шаблона в общем виде

  • определяем необходимый набор методов и свойств, которые составят общий интерфейс 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);
    }        
}

Комментарии (7) -

Ринат Муллаянов 28.08.2011 14:39:59

    Андрей, скажите, а обязательно ли отдельно выделять интерфейс IMapComponent - недостаточно ли просто абстрактного класса MapComponent(и дальше работать с ним).

    Вопрос возник после прочтения:
"Стоит еще раз отметить, что с точки зрения клиента все объекты являются экземплярами типа Component" - тогда как я понимаю клиент  будет работать не в Component, а с IComponent.

@ Ринат Муллаянов: Выделение интерфейса уменьшает связанность между частями приложения, т.к. теряется жесткая связь с конкретным классом. Однако, создание интерфейсов не является частью шаблона и можно использовать и абстрактный класс в качестве базового.

По поводу процитированного предложения - основная идея что с точки зрения клиента "все они на одно лицо". А будет это "лицо" неким базовым классом или интерфейсом уже вторично. Важно единообразие взаимодействия с ними.

Ринат Муллаянов 28.08.2011 21:26:46

@ Andrey
Спасибо, понял.

Ринат Муллаянов 29.08.2011 1:17:14

Кстати ссылки не рабочие в таблицах("Структурные шаблоны и их отличия") ну по крайней мере структурных паттернов...другие еще не смотрел

Да, спасибо что заметили. Передалал ссылки программно и не корректно настроил RegExp. Исправил.

Ринат Муллаянов 06.09.2011 21:39:47

Андрей, вы пишите:
    "Методы и свойства для операций с частями не могут поддерживаться самими частями по определению. Добавление их в общий интерфейс только утяжеляет его."
и...
    "IComponent – содержит только общие методы и свойства, не отражающие специфики различных типов объектов, входящих в реализацию Компоновщика."
    Не совсем понимаю, зачем тогда в IMapComponent содержит метод FindChild(string name) - у частей же вроде не может быть еще каких-то более мелких составных частей или я что-то не так понимаю?

Здесь речь идет о той части пользовательского кода, которая отвечает за обработку объектов (а не за создание). С её точки зрения все объекты одинаковые - реализуют IMapComponent. Часть это или составной объект – нет различия.

Разница проявляется только при создании объекта (метод создания знает про MapComposite) и скрыта от остальной части (которая работает только с IMapComponent)

Pingbacks and trackbacks (1)+

Добавить комментарий