Паттерн разработки Abstract Factory

Примеры реализации

Автор: Михаил Новиков
Источник: RSDN Magazine #4-2005
Опубликовано: 03.03.2006
Версия текста: 1.0
Generic-и и Abstract Factory
Abstract Factory на базе Prototype
Совместное использование
Атрибуты и Abstract Factory
Итоги
Список литературы

Примеры к статье

Generic-и и Abstract Factory

Паттерн Abstract Factory предоставляет интерфейс для создания семейств связанных или зависимых объектов (далее - продукты), позволяя не указывать их конкретные классы.

Одним из главных недостатков паттерна Abstract Factory (Абстрактная Фабрика) является трудоемкость добавления новых продуктов. При появлении нового продукта необходимо изменить общий интерфейс фабрик, добавив еще один метод Create*() и, соответственно, реализовать его во всех уже существующих классах фабрик. Помимо этого, для нового продукта необходимо построить собственную симметричную иерархию классов. Подобные действия следует повторить и в том случае, когда необходимо исключить продукт. Отчасти с подобной проблемой можно справиться, если реализовать Abstract Factory наподобие Acyclic Visitor, плюс, применив возможности generic-ов.


Рисунок 1. Диаграмма классов паттерна Abstract Factory.

Реализация паттерна с помощью generic-ов
      public
      interface IAbstractFactory
{
  T Create<T>();

  bool IsProduct<T>();
}

publicinterface IFactoryMethod<T>
{
  T Create();
}

publicabstractclass AbstractFactory : IAbstractFactory
{
  public T Create<T>()
  {
    IFactoryMethod<T> factoryMethod = thisas IFactoryMethod<T>;
    if (factoryMethod != null)
    {
      return factoryMethod.Create();
    }
           // Здесь можно вызвать Exception, так как// интерфейс IFactoryMethod<T> не реализован returndefault(T);
  }

  publicbool IsProduct<T>()
  {
    returnthisis IFactoryMethod<T>;
  }
}

publicclass ConcreteFactory1 : AbstractFactory,
  IFactoryMethod<ProductA>,
  IFactoryMethod<ProductB>
{  
  ProductA IFactoryMethod<ProductA>.Create()
  {
    returnnew ConcreteProductA1();
  }

  ProductB IFactoryMethod<ProductB>.Create()
  {
    returnnew ConcreteProductB1();
  }
}

Применение:

IAbstractFactory factory = new ConcreteFactory1();
ProductA productA = factory.Create<ProductA>();
ProductB productB = factory.Create<ProductB>();
// Здесь будет возвращен null, так как не реализован интерфейс IFactoryMethod<ProductC>
ProductC productC = factory.Create<ProductC>(); 

Абстрактную фабрику разбивают на отдельные фабричные методы. Предназначение каждого из них заключается в создании одного продукта. Для этого объявлен generic-интерфейс IFactoryMethod<>. Метод Create<>() интерфейса IAbstractFactory играет роль диспетчера, то есть он принимает решение, объект какого класса следует создать и вернуть, основываясь на информации о типе, переданном через аргумент generic-а.

Пусть необходимо создать фабрику, создающую продукты ProductA и ProductB. Тогда следует реализовать интерфейс IFactoryMethod<ProductA> и IFactoryMethod<ProductB>. Но в итоге получается, что в классе появляются два метода Create(). Поскольку перегрузка разрешена только с использованием сигнатур, а возвращаемое значение в сигнатуру не входит, то интерфейсы нужно реализовать явно.

Применять IFactoryMethod<> можно и без IAbstractFactory<>, например, следующим образом:

      public
      class Widget<T> where T : 
  IFactoryMethod<ProductA>,
  IFactoryMethod<ProductB>, 
  new()
{    
  public Widget()
  {
    T factory = new T();
    // Неявно приводится к IFactoryMethod<ProductA>
    CreateProductA(factory);
    // Явное приведение к IFactoryMethod<ProductB>
    IFactoryMethod<ProductB> factoryMethod = 
      (IFactoryMethod<ProductB>)factory;    
    ProductB productB = factoryMethod.Create();
  }

  privatevoid Initialize(IFactoryMethod<ProductA> factory)
  {
    ProductA productA = factory.Create();
  }    
}

Однако приходится каждый раз приводить объект типа T к IFactoryMethod<>, передавая через параметр generic-а нужные типы продуктов. От этого можно избавиться, если наложить только одно ограничение на тип T, а именно, указать, чтобы этот тип реализовывал интерфейс IAbstractFactory. В итоге можно будет вызывать generic-версию метода Create<>() и создавать продукты. Тогда потребность в непосредственном использовании IFactoryMethod отпадает.

Но совсем не обязательно, что T будет реализовывать интерфейсы так, что IAbstractFactory.Create<ProductA>() будет аналогичен вызову IFactoryMethod<ProductA>.Create(), все зависит от каждого конкретного случая. Решить эту задачу можно, если заменить интерфейс IAbstractFactory<> абстрактным классом, в котором будет уже реализован метод Create<>(), как в листинге 1.1. Тогда остается унаследовать класс фабрики от этого абстрактного класса, и реализовать необходимые IFactoryMethod<…>. Таким образом, код клиента:

      public
      class Widget<T> where T : AbstractFactory // Это абстрактный класс
  IFactoryMethod<ProductA>,
  IFactoryMethod<ProductB>, 
  new()
{
 // ...
}

гарантирует, что фабрика способна создавать продукты ProductA и ProductB, через Create<>(). Чтобы проверить, может ли фабрика создавать продукты непосредственно из кода метода, не прибегая к where, можно использовать оператор is. Для этого в интерфейсе IAbstractFactory предназначен generic-метод bool IsProduct<T>(). Если фабрика умеет создавать продукты данного типа, при передаче типа в метод будет возвращено true. В данном случае интерфейс IFactoryMethod<> используется в роли паттерна Marker Interface.

Интересную функциональность можно получить, если класс конкретной фабрики объявить как partial. Тогда появляется возможность добавлять продукты к фабрике из разных частей приложения. Это может быть полезно при использовании условной компиляции: #define, #if, #else, #endif.

Abstract Factory на базе Prototype

Допустим, требуется, чтобы во время выполнения программы пользователь сам формировал объекты продуктов, которые в дальнейшем будут использоваться в абстрактной фабрике. В случае с интерфейсом это может быть настраиваемый пользователем вид элементов управления. Тогда уместным является совместное использование паттернов Prototype (Прототип) и Abstract Factory. Вместо того, чтобы непосредственно создавать объект с помощью оператора new, фабрика будет клонировать объекты-прототипы, которые содержит в себе. Это возможно благодаря паттерну Prototype, позволяющему полиморфно создавать объекты (данный паттерн носит также название Virtual Constructor).


Рисунок 2. Диаграмма классов Абстрактной Фабрики, реализуемой на базе Прототипов.

Наложим определенные ограничения на фабрику: все продукты должны создаваться только клонированием. Для этого необходимо наложить ограничение на продукты: требуется, чтобы класс продукта реализовывал интерфейс IClonable, с помощью метода Clone() которого будет производиться клонирование объекта. Для реализации будет использоваться generic-версия абстрактной фабрики, поэтому необходимо дополнительно накладывать ограничение на типы аргументов generic-а: where T : IClonable.

Из–за использования прототипов возникает необходимость расширить функциональность фабрики. В интерфейс фабрики необходимо добавить два метода: GetPrototype<>() и SetPrototype<>(), предназначенные для получения и замены прототипа, переданного через аргумент generic-типа продукта, соответственно. Кроме того, теперь интерфейс IFactoryMethod<T> не имеет метода Create(), зато содержит свойство Prototype, которое и является прототипом продукта T, предназначенного для клонирования.

Реализация Абстрактной Фабрики на базе Прототипов.
      public
      interface IAbstractFactory
{
  T Create<T>()
    where T : ICloneable;

  bool IsProduct<T>()
    where T : ICloneable;

  void SetPrototype<T>(T prototype)
    where T : ICloneable;

  T GetPrototype<T>()
    where T : ICloneable;
}

publicinterface IFactoryMethod<T> 
  where T : ICloneable
{
  T Prototype
  {
    get;
    set;
  }
}

publicabstractclass AbstractFactory : IAbstractFactory
{    
  public T Create<T>()
    where T : ICloneable
  {
    IFactoryMethod<T> factoryMethod = thisas IFactoryMethod<T>;
    if (factoryMethod != null)
    {
      return (T)factoryMethod.Prototype.Clone();
    }
    // throw new ArgumentException();returndefault(T);
  }

  publicbool IsProduct<T>()
    where T : ICloneable
  {
    returnthisis IFactoryMethod<T>;
  }

  publicvoid SetPrototype<T>(T prototype)
    where T : ICloneable
  {
    IFactoryMethod<T> factoryMethod = thisas IFactoryMethod<T>;
    if (factoryMethod != null)
    {
      factoryMethod.Prototype = prototype;
    }
    // throw new ArgumentException();
  }

  public T GetPrototype<T>()
    where T : ICloneable
  {
    IFactoryMethod<T> factoryMethod = thisas IFactoryMethod<T>;
    if (factoryMethod != null)
    {
      return (T)factoryMethod.Prototype;
    }
    // throw new ArgumentException();returndefault(T);
  }
}

В целом создание конкретной фабрики заключается в наследовании от класса AbstractFactory и явной реализации интерфейса IFactoryMethod<> с нужными типами продуктов. Также обязательным является то, что до непосредственного создания продуктов через метод Create<>() нужно указать объекты-прототипы продуктов с помощью метода SetPrototype<>(), иначе при попытке клонирования будет вызвано исключение NullReferenceException. В остальном использование фабрики на основе прототипов не отличается от применения обычной фабрики.

Пример использования:

      public
      class GUIToolkit : AbstractFactory, IFactoryMethod<Button>
{
  private Button button;

  Button IFactoryMethod<Button>.Prototype
  {
    get { return button; }
    set { button = value; }
  }
}

StyledButton button = new StyledButton();
// изменение свойств button, // изменение стиля кнопки,// выбор цвета и прочее
IAbstractFactory factory = new GUIToolkit();
factory.SetPrototype<Button>(button);
// Клонирование кнопки
Button btn1 = factory.Create<Button>();

Совместное использование

Несложно заметить, что интерфейс фабрики, основанной на прототипах, отличается от интерфейса рассмотренной ранее generic-фабрики только методами SetPrototype<>() и GetPrototype<>(). Отсюда следует, что можно выделить наиболее общий интерфейс для всех фабрик, а именно, содержащий только методы Create<>() и IsProduct<>(). Этот интерфейс назовем IAbstractFactory. Определим интерфейс фабрики на базе прототипов, назовем его ICloneFactory. Он будет производным от IAbstractFactory, и будет содержать методы, специально предназначенные для работы с прототипами.

Аналогично, выделим общий интерфейс для фабричных методов – IFactoryMethod<>, содержащий только метод Create(). Производным от него будет являться ICloneMethod<>, главная задача которого – обеспечение доступа к прототипу через соответствующее свойство.


Рисунок 3. Диаграмма классов совместного использования продуктов разных видов.

Далее создаем абстрактный класс AbstractFactory, реализующий интерфейс IAbstractFactory, код которого рассматривался выше. Частный случаем является фабрика, основанная на использовании прототипов, поэтому класс CloneFactory одновременно наследуется от AbstractFactory и реализует интерфейс ICloneFactory.

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

Реализация одновременного использования Prototype и обычных продуктов
      public
      interface ICloneFactory : IAbstractFactory
{
  void SetPrototype<T>(T prototype) 
    where T : ICloneable;

  T GetPrototype<T>() 
    where T : ICloneable;
}

publicinterface ICloneMethod<T> : IFactoryMethod<T> 
  where T : ICloneable
{
  T Prototype
  {
    get;
    set;
  }
}

publicabstractclass CloneFactory : AbstractFactory, ICloneFactory
{    
  publicvoid SetPrototype<T>(T prototype) 
    where T : ICloneable
  {
    ICloneMethod<T> cloneMethod = thisas ICloneMethod<T>;
    if (cloneMethod != null)
    {
      cloneMethod.Prototype = prototype;
    }
    // throw new ArgumentException();
  }

  public T GetPrototype<T>() 
    where T : ICloneable
  {
    ICloneMethod<T> cloneMethod = thisas ICloneMethod<T>;
    if (cloneMethod != null)
    {
      return cloneMethod.Prototype;
    }
    // throw new ArgumentException();returndefault(T);
  }
}

Теперь, чтобы создать фабрику с такими свойствами, необходимо сделать ее наследницей CloneFactory и реализовать интерфейсы IFactoryMethod<> и ICloneMethod<>. В качестве примера приведем фабрику, создающую элементы управления.

      public
      class GUIToolkit : CloneFactory, IFactoryMethod<Edit>, ICloneMethod<Button>
{
  Edit IFactoryMethod<Edit>.Create()
  {
    returnnew StyledEdit();
  }

  private Button buttonPrototype;

  Button ICloneMethod<Button>.Prototype
  {
    get { return buttonPrototype; }
    set { buttonPrototype = value; }
  }

  Button IFactoryMethod<Button>.Create()
  {
    return (Button)buttonPrototype.Clone();
  }
}

При передаче типа Edit в Create<>() с помощью фабричного метода будет создан объект StyledEdit. При передаче Button будет клонирован объект buttonPrototype. Важно, чтобы класс Button реализовывал интерфейс IClonable.

Атрибуты и Abstract Factory

Абстрактная фабрика содержит в себе информацию о том, какие продукты и каким образом создавать. Если фабрики отличаются друг от друга только тем, какие объекты они создают, то можно описать эти зависимости с помощью атрибутов, то есть указать тип абстрактного продукта и тип продукта, который будет создаваться фактически. Такое решение наиболее естественно. За основу возьмем generic-реализацию фабрики. Следует подчеркнуть, что это является возможным из-за того, что интерфейс IAbstractFactory не зависит от IFactoryMethod<> и подобных интерфейсов.


Рисунок 4. Диаграмма классов Abstract Factory на базе атрибутов.

Реализация Abstract Factory с помощью атрибутов
// Атрибуты не могут быть генериками
[AttributeUsage(AttributeTargets.Class | 
  AttributeTargets.Struct | 
  AttributeTargets.Interface, 
  AllowMultiple = true, 
  Inherited = true)]
publicclass Product : Attribute  
{    
  // Можно использовать имя @abstract,    // но называть переменные ключевыми словами не рекомендуетсяpublic Product(Type Abstract, Type concrete)
  {
    _abstract = Abstract;
    _concrete = concrete;
  }

  private Type _abstract;

  public Type Abstract
  {
    get { return _abstract; }
    set { _abstract = value; }
  }

  private Type _concrete;

  public Type Concrete
  {
    get { return _concrete; }
    set { _concrete = value; }
  }
}

publicabstractclass AbstractFactory : IAbstractFactory
{
  public T Create<T>()
  {      
    foreach (Product product in Search<T>())
    {
      // Важно, чтобы существовал конструктор без параметровreturn (T)product.Concrete.GetConstructor(Type.EmptyTypes).Invoke(null);
    }  
    // throw new ArgumentException();returndefault(T);
  }

  private IEnumerable<Product> Search<T>()
  {
    Product[] products = (Product[])GetType().GetCustomAttributes(typeof(Product), true);
    Type type = typeof(T);
    foreach (Product product in products)
    {
      if (type == product.Abstract)
      {
        yield return product;
      }
    }
  }

  publicbool IsProduct<T>()
  {
    return Search<T>().GetEnumerator().MoveNext();
  }
}

Одним из главных недостатков данной реализации является низкая скорость. Это вызвано, прежде всего, тем, что для получения информации об атрибутах и создании продукта используется Reflection. То есть при каждом вызове метода Create<>() во время выполнения происходит поиск среди атрибутов и выбирается нужный. Это может быть накладно в случае большого количества создаваемых продуктов. В качестве решения этой проблемы можно применить отложенную инициализацию массива с атрибутами Product. В результате, вместо того, чтобы каждый раз вызывать GetType() и GetCustomAttribute(), поиск будет осуществляться в атрибутах, уже полученных ранее.

Пример использования:

[Product(typeof(Button), typeof(MotifButton))]
[Product(typeof(Label), typeof(MotifLabel))]
publicclass MotifToolkit : AbstractFactory
{
}
IAbstractFactory factory = new MotifToolkit();
Button button = factory.Create<Button>();
Label label = factory.Create<Label>();

Итоги

Выбор версии паттерна Abstract Factory, подходящей по скорости и функциональности, зависит от поставленных задач. Важно отметить, что благодаря общему интерфейсу IAbstractFactory можно использовать любой из них при минимальном изменении кода.

Список литературы


Эта статья опубликована в журнале RSDN Magazine #4-2005. Информацию о журнале можно найти здесь