Каждый прогер наверняка использовал паттерн «Компоновщик», а большинство из нас также сталкивалось с необходимостью реализовать его в своем проекте. И часто так получается, что каждая его реализация налагает особые требования на определяемую бизнес-логику, при этом с точки зрения работы с иерархической структурой мы хотим иметь одинаково широкий набор возможностей: одних методов Add и Remove часто недостаточно, так почему бы не добавить Contains, Clear и с десяток других? А если еще нужны специальные методы обхода поддеревьев через итераторы? И вот такую функциональность хочется иметь для различных независимых иерархий, а также не обременять себя необходимостью определять реализацию таких методов в каждом из множества элементов Composite. Ну и листовые компоненты тоже не помешало бы упростить.

Чуть ниже я предложу свой вариант решения такой проблемы, применительно к возможностям C#.


Итак, мы имеем интерфейсный тип Component, который перегружен двумя областями ответственности: одна определяет бизнес-логику — то, ради чего и строилась иерархия; вторая обеспечивает прозрачное взаимодействие в иерархии и управляет потомками для композитных элементов. Попроб��ем одну их них вынести в отдельный интерфейс, к которому можно будет получать доступ по свойству компонента Children (или методу GetChildren). В объекте, который возвращается свойством, будут собраны все операции над коллекцией, включая перечисление, добавление и удаление дочерних элементов, а также все, что нам заблагорассудится.



Мы также определили свойство (метод) IsComposite, чтобы получить быструю и хорошо читаемую проверку на то, является ли элемент композитным или же листовым. Этим свойством можно и не пользоваться: тогда при попытке изменить коллекцию дочерних элементов для листового компонента будет выбрасываться исключение NotSupportedException. Таким образом, мы не теряем прозрачность интерфейса для всех компонентов — основное преимущества паттерна «Компоновщик», — и в то же время получаем простой способ определить, могут ли у любого выбранного компонента быть дочерние элементы.

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

using System.Collections.Generic;
using System.Diagnostics.Contracts;
namespace ComponentLibrary
{
  [ContractClass(typeof(IComponentContract))]
  public interface IComponent<out TComponent, out TChildrenCollection>
     where TComponent : IComponent<TComponent, TChildrenCollection>
     where TChildrenCollection : class, IEnumerable<TComponent>
  {
     TChildrenCollection Children { get; }
     bool IsComposite { get; }
  }
}


* This source code was highlighted with Source Code Highlighter.

От чего интерфейс реализован как шаблонный и что за параметры-типы он принимает? На самом деле идея очень проста: привнести строгую типизацию туда, где действительные типы еще не известны.

TComponent — это всего лишь фактический тип того дочернего интерфейса (или класса в более тесно связанной архитектуре), который вы унаследуете от IComponent, чтобы добавить в него обязанности типа Operation( ).

TChildrenCollection — интерфейс коллекции, который вы реализуете, чтобы обращаться к дочерним элементам. В этом интерфейсе должен быть определен как минимум только GetEnumerator( ), через который можно получить итератор коллекции. Дело в том, что иногда не нужно предоставлять методы типа Add( ) и Remove( ), т.к. все элементы могут добавляться в конструкторе, а сама иерархическая структура не должна изменяться после создания. А если вам вдруг понадобятся уведомления об изменении коллекции — передайте ObservableCollection<TComponent> в качестве TChildrenCollection, и дело в шляпе!

В контракте типа мы укажем следующие ограничения на возвращаемое значение свойства Children: 1) возвращаемая коллекция не равна null; 2) ни один из ее элементов не равен null; 3) компонент либо указан как композитный, либо не содержит дочерних элементов. Для краткости код контракта здесь н�� приводится.

Помните, мы говорили, что хотели бы наделить каждый компонент дополнительным методом, возвращающим итератор, реализующий сложную логику обхода поддерева? Допустим, что мы написали такой итератор ComponentDescendantsEnumerator<TComponent> (его код можно скачать по ссылке в конце статьи), а затем обернули его в класс ComponentDescendantsEnumerable<TComponent>, определяющий IEnumerable<TComponent>. Теперь нужно решить, где разместить методы, возвращающие подобные итераторы? К счастью в C# есть очень полезный механизм — методы-расширения. Давайте попробуем его применить.

namespace ComponentLibrary.Extensions
{
  public static class ComponentExtensions
  {
     public static IEnumerable<T> GetDescendants<T>(this T component)
         where T : IComponent<T, IEnumerable<T>>
     {
         // здесь был контракт метода
         return new ComponentDescendantsEnumerable<T>(component);
     }
  }
}


* This source code was highlighted with Source Code Highlighter.

Мы реализуем метод-расширение в отдельном пространстве имен — так мы сможем его вызывать словно метод, принадлежащий интерфейсу IComponent< , >, лишь тогда, когда импортируем это пространство имен.

Далее перед нами еще задача: нужно реализовать способ получения коллекций, которые никогда не содержат элементов, а на все запросы изменения (вроде Add / Remove) выбрасывают NotSupportedException. Сначала сделаем одну такую коллекцию, реализующую ICollection<T>. Однако, если коллекция никогда не изменяется и создается всегда пустой, то нет смысла делать более одной такой коллекции на всю программу (вернее на AppDomain). Идеальный случай, чтобы воспользоваться паттерном «Синглтон»! (реализацию класса Singleton<T> можно узнать в прилагаемых исходниках)

sealed internal class ItemsNotSupportedCollection<T> :
  Singleton<ItemsNotSupportedCollection<T>>,
  ICollection<T>
{
  private ItemsNotSupportedCollection() { }

  public int Count { get { return 0; } }

  public bool IsReadOnly { get { return true; } }

  public bool Contains(T item) { return false; }

  public void CopyTo(T[] array, int arrayIndex) { }

  public void Add(T item) { throw new NotSupportedException(); }

  public void Clear() { throw new NotSupportedException(); }

  public bool Remove(T item) { throw new NotSupportedException(); }

  public IEnumerator<T> GetEnumerator()
  {
     return ItemsNotSupportedEnumerator<T>.Instance;
  }

  IEnumerator IEnumerable.GetEnumerator()
  { return this.GetEnumerator(); }
}


* This source code was highlighted with Source Code Highlighter.

Ради прозрачности используемый класс итератора представляет итератор пустой коллекции, поэтому тоже достаточно одного его экземпляра — вновь синглтон.

sealed internal class ItemsNotSupportedEnumerator<T> :
  Singleton<ItemsNotSupportedEnumerator<T>>,
  IEnumerator<T>
{
  private ItemsNotSupportedEnumerator() { }

  public T Current { get { return default(T); } }

  public void Dispose() { }

  object IEnumerator.Current { get { return null; } }

  public bool MoveNext() { return false; }

  public void Reset() { throw new NotSupportedException(); }
}


* This source code was highlighted with Source Code Highlighter.

Осталось только создать статическое свойство, видимое извне сборки и возвращающее доступную только для чтения коллекцию элементов для интерфейса ICollection<T>.

public static class ComponentCollections<TComponent>
  where TComponent : IComponent<TComponent, IEnumerable<TComponent>>
{
  public static ICollection<TComponent> EmptyCollection
  {
     get
     {
         // здесь был контракт
         return ItemsNotSupportedCollection<TComponent>.Instance;
     }
  }
}


* This source code was highlighted with Source Code Highlighter.

Пришло время для самого интересного: применение нашей мини-библиотеки для создания конкретной иерархии классов. Допустим надо организовать систему меню, состоящую из: MenuCommand — конкретная команда, и Menu — подменю, которое может содержать другие команды и подменю. Все классы расположены в отдельной сборке. Шестое чувство подсказывает, что паттерн «Компоновщик» пришелся бы здесь кстати.



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



namespace MenuLibrary
{
  public interface IMenuItem :
     IComponent<IMenuItem, ICollection<IMenuItem>>
  {
     string Name { get; }
     void Display(int indent = 0);
  }
}


* This source code was highlighted with Source Code Highlighter.

В качестве параметра-типа TComponent мы всегда передаем интерфейс компонентов (т.е. этот же IMenuItem), в качестве TChildrenCollection — интерфейс реализуемой коллекции. Мы могли бы для TChildrenCollection создать свой интерфейс, определяющий методы Add, Remove и GetChild (а также GetEnumerator), как и в классическом варианте паттерна. Можно передать например IList<IMenuItem>, но мы решили, что здесь нам подходит стандартный интерфейс ICollection<IMenuItem>.

Вот так мы определим листовой компонент MenuCommand:

public class MenuCommand : IMenuItem
{
  private readonly string name;

  public MenuCommand(string name)
  {
     // здесь был контракт
     this.name = name;
  }

  public string Name { get { return this.name; } }

  public void Display(int indent = 0)
  {
     string indentString = MenuHelper.GetIndentString(indent);
     Console.WriteLine("{1}{0} [Command]", this.name, indentString);
  }

  public ICollection<IMenuItem> Children
  {
     get { return ComponentCollections<IMenuItem>.EmptyCollection; }
  }

  public bool IsComposite { get { return false; } }
}


* This source code was highlighted with Source Code Highlighter.

Свойство Children возвращает ранее объявленную «синглтоновую» коллекцию для листовых элементов. Теперь объявим композитный компонент Menu.

public class Menu : IMenuItem
{
  private readonly ICollection<IMenuItem> children =
     new List<IMenuItem>();

  private readonly string name;

  public Menu(string name)
  {
     // здесь должен быть контракт
     this.name = name;
  }

  public string Name { get { return this.name; } }

  public void Display(int indent = 0)
  {
     string indentString = MenuHelper.GetIndentString(indent);
     Console.WriteLine("{1}{0} [Menu]", this.name, indentString);
     int childrenIndent = indent + 1;
     foreach (IMenuItem child in this.children)
     {
         child.Display(childrenIndent);
     }
  }

  public ICollection<IMenuItem> Children
  { get { return this.children; } }

  public bool IsComposite { get { return true; } }
}


* This source code was highlighted with Source Code Highlighter.

Children неожиданно возвращает использованную стандартную коллекцию List<T>. Это хороший ход, если пользователю нашей иерархии разрешено привести тип объекта Children к List<T> и использовать все дополнительные возможности этого класса. Но если такой расклад недопустим, то надо обернуть List<T> в некий внутренний класс, реализующий только ICollection<T> и недоступный другим сборкам (или даже классам).

Теперь протестируем написанный нами код.

using System;
using System.Linq;
using ComponentLibrary.Extensions;
using MenuLibrary;
namespace MenuTest
{
  public static class MenuTest
  {
     public static void Perform()
     {
         // создаем структуру меню
         IMenuItem rootMenu = new Menu("Root");
         // ... меню File
         IMenuItem fileMenu = new Menu("File");
         fileMenu.Children.Add(new MenuCommand("New"));
         fileMenu.Children.Add(new MenuCommand("Open"));
         // ... меню File->Export
         IMenuItem fileExportMenu = new Menu("Export");
         fileExportMenu.Children.Add(new MenuCommand("Text Document"));
         fileExportMenu.Children.Add(new MenuCommand("Binary Format"));
         fileMenu.Children.Add(fileExportMenu);
         // ... меню File
         fileMenu.Children.Add(new MenuCommand("Exit"));
         rootMenu.Children.Add(fileMenu);
         // ... меню Edit
         IMenuItem editMenu = new Menu("Edit");
         editMenu.Children.Add(new MenuCommand("Cut"));
         editMenu.Children.Add(new MenuCommand("Copy"));
         editMenu.Children.Add(new MenuCommand("Paste"));
         rootMenu.Children.Add(editMenu);
         // выводим меню на экран
         rootMenu.Display();
         Console.WriteLine();
         // выводим на консоль имена всех составных меню,
         // вложенных в Root, начинающихся на буквы "E" или "R"
         var compositeMenuNames =
            from menu in rootMenu.GetDescendants()
            where menu.IsComposite
              && (menu.Name.StartsWith("E") || menu.Name.StartsWith("R"))
            select menu.Name;
         foreach (string menuName in compositeMenuNames)
         {
            Console.WriteLine(menuName);
         }
     }
  }
}


* This source code was highlighted with Source Code Highlighter.

Обратите внимание на LINQ-запрос к перечислению, возвращаемому методом-расширением GetDescendants(). Взглянем на результат работы.



Вот так все просто. Причем в логике разработанных компонентов все внимание проектировщика сосредоточено на построении бизнес-логики, а не иерархической структуры или классов-контейнеров.

Ссылка на исходники: http://www.fileden.com/files/2011/10/7/3205975/ComponentLibrary.zip

P.S. Если вам что-то показалось не очевидным, либо вы хотели бы посмотреть на вариант реализации иерархии, доступной только для чтения, то можете обратиться к расширенному варианту этого же поста.

P.P.S. Если честно, я надеюсь, что в комментах кто-нибудь предложит лучший вариант реализации Компоновщика, чем у меня.

UPD Как правильно заметил avalter, здесь я всего лишь применил к Компоновщику Extract Interface, Extract Class и использовал NullObject (желательно закончить Extract Class, вынести используемую коллекцию из композитных компонентов и инкапсулировать ее в отдельный класс). В итоге получается не просто Компоновщик, а более гибкая структура.

Реализовывать каждый раз паттерн таким образом «с нуля» ни в коем случае не советую! Но можно взять код сборки ComponentLibrary, скопировать ее в свой проект и автоматически получить некоторые преимущества для своей иерархии: готовые Null-Object коллекции для листовых элементов, дополнительные итераторы для обхода структуры, а также контракт на интерфейс IComponent, о котором тут упоминалось вскользь. Т.о. при реализации очередной иерархии, подобной IMenuItem, можно задумываться лишь над логикой методов Display(), если реализованных в ComponentLibrary интерфейсов структур достаточно (иначе определить свои реализации).