пятница, 7 октября 2011 г.

Как паттерн Компоновщик подружился с .NET

Наверняка каждый прогер знаком с паттерном "Компоновщик", неоднократно с ним встречался и реализовывал в каждой второй своей программе. Но всегда ли мы реализуем его "как в книжке", или же немного иначе? Как достичь еще большей эффективности при его использовании в приложениях на конкретном языке, например на C#? В этом посте я не открою Америку, не изобрету велосипед, а просто попытаюсь реализовать паттерн более-менее разумным способом для решения более-менее широкого круга задач.

Компоновщик - очень полезный паттерн, позволяющий воспринимать часть как целое, а целое как часть. Это обеспечивает нас возможностью взаимодействовать с иерархическими структурами практически неограниченного уровня вложенности элементов (компонентов). При этом все компоненты имеют прозрачный интерфейс, поэтому, используя компоновщик, мы освобождаемся от необходимости постоянно сверять фактический тип элемента с ожидаемым. Паттерн как бы говорит "все просто: используй Operation( ), а об остальном позаботятся сами компоненты".



Composite.gif


Но, как обычно бывает, и здесь не обошлось без ложки дёгтя, а вернее даже без нескольких увесистых ложек. Во-первых, посмотрите на интерфейс Component, он вас действительно устраивает? Представьте на секунду, что дерево имеет десятки разновидностей компонентов Leaf и Composite, отличающихся реализацией Operation( ), а не положенного в основе контейнера - в 9 случаях из 10 вас устроит стандартный List<T>. Но нет, методы Add, Remove и т.п. должны либо заново реализовываться в каждом классе, либо необходимо выделить общего наследника и повторно использовать код через него, что тоже не всегда удобно.


Во-вторых GetChild (in index : int) раскрывает кое-что о реализации контейнера - оказывается он индексируемый и любой элемент можно получить по номеру. С этой проблемой конечно же давно справились - в интерфейсе появляется метод CreateIterator, который возвращает (как ни странно) итератор, перебирающий дочерние элементы в некотором произвольном порядке.


Но что если вы уже имеете диаграмму из 50 типов-наследников Component, а потом внезапно к вам приходит просветление - в интерфейсе Component не хватает метода Contains! А еще бы не помешали Clear, ну и AddRange, а еще... В результате и так немаленький интерфейс Component раздувается до двух десятков методов, каждый из которых нужно реализовать во всех его имплементациях.


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


В итоге интерфейс перегружен двумя областями ответственности: одна определяет бизнес-логику - то, ради чего и строилась иерархия; вторая обеспечивает прозрачное взаимодействие в иерархии и управляет потомками для композитных узлов. Реализовать, а потом использовать такой интерфейс уже непросто, а если его нужно заново спроектировать и учесть массу "подводных камней" в применяемом контексте... Особенно если это приходится делать часто...


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

Как известно, современные языки предоставляют широкие возможности по реализации сложных ООП-структур. Я выбрал для выполнения поставленной выше задачи .NET и язык C# просто потому, что мне приходится их использовать чаще других языков и платформ, ну и знаю я их достаточно, чтобы писать красиво выглядящий быдлокод :-) Нечто подобное можно написать на VB, Java, а может и на C++. Или даже на Smalltalk! ;-) Но все-таки то, о чем я далее расскажу, паттерном назвать никак нельзя, это просто воплощение компоновщика в реалиях современного .NET.

Несколько строк назад мы задумались над тем, что классический компоновщик совмещает две области ответственности. Так давайте одну из них вынесем в отдельный класс, к которому можно будет получать доступ по свойству компонента Children (или методу GetChildren). В объекте, который возвращается свойством, будут собраны все операции над коллекцией, включая перечисление, добавление и удаление дочерних элементов, а также все, что нам заблагорассудится. [Примечание: место, куда подевались методы создания недостающих итераторов, будет показано чуть позже].


Теперь опять вернемся к нашему паттерну. Что будет, если для листового компонента (Leaf) попытаться добавить или удалить дочерний элемент? Правильно, во время выполнения должно генерироваться NotSupportedException, поскольку прозрачность интерфейса для всех видов компонентов - одна из главных целей использования "компоновщика". Но строить логику работу на перехвате исключений или надеяться на "авось элемент не листовой" - нехорошо, хотя бы из-за снижения быстродействия в процессе генерации/перехвата исключения.

Поэтому способ выполнить проверку перед изменением дочерней коллекции компонента явно не лишний. Наш паттерн предлагает (да-да, у паттерна есть свое мнение!) выполнять проверку приведением типа. Вот если тип объекта приводится к листовому, то вызывать метод Add крайне не рекомендуется. А если разновидностей и Leaf, и Composite узлов довольно много (и они не имеют общих наследников - все-таки мы программируем на уровне интерфейса, а не реализации), то проверка приведением типов будет очень сложной и неэффективной. Так давайте добавим в интерфейс Component новое свойство (или метод) IsComposite! Для листовых элементов оно возвращает false, а для составных - true. В итоге проверка быстрая и хорошо читаемая.


Итак, еще раз: наша задача написать некоторый повторно используемый код, который поможет в будущем упростить реализацию паттерна "Компоновщик". Так вот, попробуем определить интерфейс компонента - 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 : classIEnumerable<TComponent>
  {
    TChildrenCollection Children { get; }
    bool IsComposite { get; }
  }
}


В этом коде уже может быть кое-что неясно. Начнем с пространства имен System.Diagnostics.Contracts: оно позволяет задействовать Code Contracts - технологию Microsoft Research, официально включенную в состав .NET 4.0 BCL (Base Class Library). С помощью Code Contracts вы сможете программировать по контракту - определять предусловия (метод Contract.Requires), постусловия (метод Contract.Ensures) и инварианты (атрибут ContractInvariantMethod). Я не буду подробнее останавливаться на этом, но в нашем случае контракты позволят наложить ограничения на интерфейсы: все реализации таких интерфейсов должны неукоснительно следовать контракту.

Теперь другой вопрос: от чего интерфейс реализован как шаблонный и что за параметры-типы он принимает? На самом деле идея очень проста: привнести строгую типизацию туда, где действительные типы еще не известны. TComponent - это всего лишь фактический тип того дочернего интерфейса (или класса в более тесно связанной архитектуре), который вы унаследуете от IComponent, чтобы добавить в него обязанности типа Operation( ). Параметры-типы объявлены как ковариантные, поэтому возможно такое неявное приведение между интерфейсами, при котором интерфейс с более производным параметром-типом может быть присвоен интерфейсу с таким же или менее производным параметром-типом.

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

Сейчас присмотритесь: может ли TComponent быть типом значения (struct)? Да, может, причем за счет строгой типизации его упаковки в ссылочный интерфейсный тип происходить не будет. Однако тогда теряется смысл паттерна "Компоновщик": все компонуемые объекты обязаны быть композитными, т.к. типы значения не могут иметь наследников (а только из листовых узлов иерархию не построишь). А вот TChildrenCollection должен быть ссылочным типом: в противном случае после каждого вызова геттера свойства Children будет возвращена копия, что очень плохо. Да и вообще здесь применение значимых типов излишне, т.к. такие типы необходимо делать неизменяемыми во избежание неожиданностей, а это уменьшает ценность применения нашего паттерна.

Теперь рассмотрим не менее важную часть интерфейса - его контракт, определяемый в отдельном классе.

namespace ComponentLibrary
{
  [ContractClassFor(typeof(

    IComponent<IComponentContract, IEnumerable<IComponentContract>>))]
  internal abstract class IComponentContract :
    IComponent<IComponentContract, IEnumerable<IComponentContract>>
  {
    private IComponentContract() { }

    public abstract bool IsComposite { get; }

    public IEnumerable<IComponentContract> Children
    {
      get
      {
        Contract.Ensures(
Contract
          .Result<IEnumerable<IComponentContract>>() != null);
        Contract.Ensures(Contract.ForAll(
          Contract.Result<IEnumerable<IComponentContract>>(),
          item => item != null));
        Contract.Ensures(
          this.IsComposite ||

          !Contract.Result<IEnumerable<IComponentContract>>().Any());
        throw new NotSupportedException();
      }
    }
  }
}

Единственное, что здесь важно - это контракт на возвращаемое значение свойства Children. Все наши реализации, которые будут наследовать интерфейс IComponent< , >, должны удостовериться, что: 1) возвращаемая ими коллекция не равна null; 2) ни один из ее элементов не равен null; а также 3) компонент либо указан как композитный, либо не содержит дочерних элементов.

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

Теперь нужно решить, где разместить методы, возвращающие подобные итераторы? Если в интерфейсе коллекции TChildrenCollection, то нельзя будет использовать стандартные интерфейсы вида ICollection<T>, а также не получится логично определить итератор, возвращающий перечисление текущего компонента и некоторых вложенных (т.к. в интерфейсе TChildrenCollection отсутствует обратная ссылка на TComponent). Если в интерфейсе IComponent< , >, то придется реализовывать их в каждом определяющем его классе. К счастью в C# есть очень полезный механизм - методы-расширения. Давайте попробуем его применить.

namespace ComponentLibrary.Extensions
{
  public static class ComponentExtensions
  {
    public static IEnumerable<T> GetDescendants<T>(this T component)
      where T : IComponent<T, IEnumerable<T>>
    {
      if (component == null)
      {
        throw new ArgumentNullException("component");
      }
      Contract.Ensures(Contract.Result<IEnumerable<T>>() != null);
      Contract.Ensures(Contract.ForAll(
        Contract.Result<IEnumerable<T>>(),
        item => item != null));
      Contract.EndContractBlock();
      return new ComponentDescendantsEnumerable<T>(component);
    }
  }
}

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

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

internal abstract class Singleton<T>
  where T : Singleton<T>
{
  private static class singletonCreator
  {
    private static readonly T instance =
      (T)typeof(T)
        .GetConstructor(
          bindingAttr: BindingFlags.Instance | BindingFlags.NonPublic,
          binder: null,
          types: new Type[0],
          modifiers: new ParameterModifier[0])
        .Invoke(null);

    public static T Instance
    {
      get
      {
        Contract.Ensures(Contract.Result<T>() != null);
        return singletonCreator.instance;
      }
    }
  }

  protected Singleton() { }

  public static T Instance
  {
    get
    {
      Contract.Ensures(Contract.Result<T>() != null);
      return singletonCreator.Instance;
    }
  }
}

Теперь, наследуя класс коллекции от Singleton<T> и реализуя закрытый конструктор, можно автоматически получить строготипизированное статическое свойство Instance, возвращающее экземпляр синглтона с учетом отложенной и потокобезопасной инициализации. Попробуем написать класс коллекции.

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(); }
}

Этот класс имеет область видимости уровня сборки (assembly), поэтому будет скрыт от других сборок (К.О.). Ради прозрачности используемый класс итератора представляет итератор пустой коллекции, поэтому тоже достаточно одного его экземпляра - вновь синглтон.

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(); }
}

Осталось создать пару статических свойств, видимых извне сборки и возвращающих пустую доступную только для чтения коллекцию элементов для интерфейсов ICollection<T> и IEnumerable<T> (для последнего все необходимое уже есть в классах LINQ, поэтому мы реализуем лишь синглтон).

public static class ComponentCollections<TComponent>
where TComponent : IComponent<TComponent, IEnumerable<TComponent>>
{
  private static class emptyEnumerableCreator
  {
    private static readonly IEnumerable<TComponent> instance =
      Enumerable.Empty<TComponent>();
 
    public static IEnumerable<TComponent> Instance
    {
      get
      {
        Contract.Ensures(
          Contract.Result<IEnumerable<TComponent>>() != null);
        return emptyEnumerableCreator.instance;
      }
    }
  }
 
  public static IEnumerable<TComponent> EmptyEnumerable
  {
    get
    {
      Contract.Ensures(
        Contract.Result<IEnumerable<TComponent>>() != null);
      return emptyEnumerableCreator.Instance;
    }
  }
 
  public static ICollection<TComponent> EmptyCollection
  {
    get
    {
      Contract.Ensures(
        Contract.Result<ICollection<TComponent>>() != null);
      return ItemsNotSupportedCollection<TComponent>.Instance;
    }
  }
}
Пришло время для самого интересного: применение нашей мини-библиотеки для создания конкретной иерархии классов. Допустим надо организовать систему меню, состоящую из: MenuCommand - конкретная команда, и Menu - подменю, которое может содержать другие команды и подменю. Все классы расположены в отдельной сборке. Шестое чувство подсказывает, что паттерн "Компоновщик" пришелся бы здесь кстати.


Сначала определим интерфейс, общий для всех классов иерархии.

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

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


В качестве параметра-типа 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)
  {
    if (!MenuRules.NameIsValid(name))
    {
      throw new ArgumentException(

        "Invalid name for MenuCommand.""name");
    }
    Contract.EndContractBlock();
    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; } }
}


Здесь Name и Display реализуют некую логику, Children возвращает ранее объявленную "синглтоновую" коллекцию для листовых элементов, а IsComposite просто возвращает false (так мы легко отличим этот компонент от композитного). Еще раз повторюсь: мы не обязаны для каждого элемента проверять IsComposite, вполне допустимо обращаться прямо к свойству Children так, словно это нормальная коллекция.

Объявим композитный компонент Menu:

public class Menu : IMenuItem
{
  private readonly ICollection<IMenuItem> children =

    new List<IMenuItem>();

  private readonly string name;

  public Menu(string name)
  {
    if (!MenuRules.NameIsValid(name))
    {
      throw new ArgumentException("Invalid name for Menu.""name");
    }
    Contract.EndContractBlock();
    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; } }
}


Итак, Name реализован так же, как и для MenuCommand; Display выводит представление как для себя, так и для всех дочерних элементов; IsComposite всегда возвращает true - компонент-то композитный. А вот 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",

                StringComparison.CurrentCulture)
           || menu.Name.StartsWith("R",

                StringComparison.CurrentCulture))
        select menu.Name;
      foreach (string menuName in compositeMenuNames)
      {
        Console.WriteLine(menuName);
      }
    }
  }
}

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


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

namespace MenuLibrary.ReadOnly
{
  [ContractClass(typeof(IMenuItemContract))]
  public interface IMenuItem :
    IComponent<IMenuItem, IEnumerable<IMenuItem>>
  {
    string Name { get; }
    void Display(int indent = 0);
  }



  public class MenuCommand : IMenuItem
  {
    private readonly string name;

    public MenuCommand(string name)
    {
      if (!MenuRules.NameIsValid(name))
      {
        throw new ArgumentException(

          "Invalid name for MenuCommand.""name");
      }
      Contract.EndContractBlock();
      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 read-only]",

        this.name, indentString);
    }

    public IEnumerable<IMenuItem> Children
    {
      get return ComponentCollections<IMenuItem>.EmptyEnumerable; }
    }

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



  public class Menu : IMenuItem
  {
    private readonly IEnumerable<IMenuItem> children;
    private readonly string name;

    public Menu(string name, params IMenuItem[] children)
    {
      if (!MenuRules.NameIsValid(name))
      {
        throw new ArgumentException("Invalid name for Menu.""name");
      }
      Contract.EndContractBlock();
      this.name = name;
      IMenuItem[] notNullChildren = children ?? 
new IMenuItem[0];
      this.children =

        new ReadOnlyCollection<IMenuItem>(notNullChildren);
    }

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

    public void Display(int indent = 0)
    {
      string indentString = MenuHelper.GetIndentString(indent);
      Console.WriteLine("{1}{0} [Menu read-only]",

        this.name, indentString);
      int childrenIndent = indent + 1;
      foreach (IMenuItem child in this.children)
      {
        child.Display(childrenIndent);
      }
    }

    public IEnumerable<IMenuItem> Children

      { get { return this.children; } }

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

Все идентично первому примеру, но параметр-тип TChildrenCollection задан как IEnumerable<IMenuItem>, свойство Children для компонента MenuCommand возвращает ComponentCollections<IMenuItem>.EmptyEnumerable, а это же свойство для класса Menu возвращает коллекцию ReadOnlyCollection<IMenuItem> по интерфейсу IEnumerable<IMenuItem>. Также конструктор Menu принимает массив дочерних элементов IMenuItem. Попробуем использовать такую иерархию.

public static class ReadOnlyMenuTest
{
  public static void Perform()
  {
    // создаем структуру меню
    IMenuItem rootMenu =
      new Menu("Root",
        new Menu("File",
          new MenuCommand("New"),
          new MenuCommand("Open"),
          new Menu("Export",
            new MenuCommand("Text Document"),
            new MenuCommand("Binary Format")),
          new MenuCommand("Exit")),
        new Menu("Edit",
          new MenuCommand("Cut"),
          new MenuCommand("Copy"),
          new MenuCommand("Paste")));
    // выводим меню на экран
    rootMenu.Display();
    Console.WriteLine();
    // выводим на консоль имена всех составных меню, включая сам Root,
    // начинающихся на буквы "E" или "R"
    var compositeMenuNames =
      from menu in rootMenu.GetSelfAndDescendants()
      where menu.IsComposite
        && (menu.Name.StartsWith("E", StringComparison.CurrentCulture)
        || menu.Name.StartsWith("R", StringComparison.CurrentCulture))
      select menu.Name;
    foreach (string menuName in compositeMenuNames)
    {
      Console.WriteLine(menuName);
    }
  }
}

Да, мы только что очень быстро реализовали совершенно другую иерархию классов с другим интерфейсом взаимодействия с дочерними элементами на основе интерфейса IComponent< , >. Причем в логике разработанных компонентов все внимание проектировщика сосредоточено на построении бизнес-логики, а не иерархической структуры или классов-контейнеров. [Примечание: GetSelfAndDescendants - еще один метод-расширение, его код вы найдете в прикрепленных файлах]. Посмотрим на результат работы.


Вот так легко и непринужденно паттерн "Компоновщик" вошел в нашу жизнь и заметно ее упростил :-)

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

7 комментариев:

  1. Не осилил до конца. Достаточно написать
    abstract class Node
    {
    public readonly List Children = new List();
    }

    И от этого класса наследовать свою иерархию. Зачем такие сложности?

    ОтветитьУдалить
  2. Чтобы отделить структуру иерархии от бизнес-логики. А если вы завтра захотите заменить List на ObservableCollection, то как поступить? Например заменить List на IList.

    Потом вы реализовали GetDescendants, возвращающий итератор для всего поддерева. А если нужно будет создать другую иерархию, которая в своей основе содержит какой-нибудь IMyCollection? И в ней тоже хочется использовать этот итератор, а код менять нельзя.

    Ну вот я и выделил общий интерфейс для всех этих иерархий в IComponent, только и всего. А хорошо здесь то, что весь этот код я уже написал, и его можно без изменений использовать в 90% реализаций компоновщика для самых разных случаев. Если в проекте с 10 иерархий, абсолютно различных по реализации, но в них нужно привнести общее поведение, то такие вот сложности помогают.

    ОтветитьУдалить
  3. Другими словами, ваш код можно заменить на

    INode : IComponent<INode, List> { }

    Node : INode
    {
    private readonly List children;
    public List Children { get { return children; } }
    }

    LeafNode : INode
    {
    public List Children { get {
    // возврат NullObject-коллекции, которая определяется 1 раз для всех List-иерархий
    } }
    }

    И теперь для любого INode работает итератор GetDescendants().

    ОтветитьУдалить
  4. Да, это не панацея и не новая трактовка паттерна. Да и никакой отсебятины я не придумал, просто использовал другие паттерны, кроме компоновщика. Надеюсь ответил :-)

    ОтветитьУдалить
  5. Дело в том, что количество инфраструктурного кода неоправданно велико. Простейшая иерархия превращается в нечто монструозное.
    Если же понадобится специфическое поведение, то дело может не ограничиться простой заменой List на MyCollection, нужно будет в конструктор передать какие-то параметры, запомнить что-то в полях и т.д. В большинстве случаев проще начать новую иерархию, благо базовый класс очень прост (просто объявить публичную коллекцию Children), чего не скажешь про ваш вариант.
    Вы слишком идеализируете паттерны, это просто инструмент при проектировании, прививающий навык мыслить "объектно", не стоит ради "реализации паттерна" городить такие конструкции.
    Вот хорошее мнение на этот счет:
    http://rsdn.ru/article/patterns/gotopatterns.xml#ECE

    ОтветитьУдалить
  6. Вы не поверите, но я ярый противник необдуманного применения паттернов, каждый из них имеет свои отрицательные стороны: усложняет дизайн, негативно сказывается на производительности. Я считаю, что если система точно не будет изменяться со временем, то никакой компоновщик ей не нужен.

    Но когда пишешь код для некоторого большого компонента, для которого требования очень вероятно изменятся, то хочется упростить себе жизнь. Я именно это и сделал: создал некоторую базу кода (пусть большую, но она пишется 1 раз), которая совершенно не снижает гибкости (вы по-прежнему можете "запомнить в полях и передать в конструктор") и дает возможность расшарить функционал между иерархиями.

    Если вы просто хотите реализовать Компоновщик - НЕ НАДО делать так, как я написал. Я к этому решению пришел, когда была иерархия > 20 классов, и нужно было создать другую иерархию с нуля, скопировав часть функциональности. Но просто копировать код плохо, поэтому пришлось выделить общий интерфейс. Поэтому, если считаете открытое поле Children достаточным решением - используйте его. Если гибкость компоновщика недостаточно - я показал, как его улучшить его реализацию.

    ОтветитьУдалить