Сообщений 2    Оценка 71 [+0/-1]         Оценить  
Система Orphus

KeyedFactory

Автор: Юрий Королев
Источник: RSDN Magazine #1-2008
Опубликовано: 17.07.2008
Исправлено: 10.12.2016
Версия текста: 1.0
Factory Method
Время создания объектов
Выводы
Список литературы

После появления книги 
«Приемы объектно-ориентированного проектирования -
паттерны проектирования» [1] наше понимание того,
как наилучшим образом представить
паттерны проектирования,
значительно возросло. Гради Буч

Factory Method

Фабричный метод (Factory Method) – паттерн, порождающий объекты. Данный шаблон определяет интерфейс для создания объекта и позволяет подклассам принимать решение, какой именно объект создавать. Структура шаблона приведена на рисунке 1.


Рисунок 1. Структура шаблона Factory Method.

Участники шаблона:

  1. Product – продукт. Определяет интерфейс объектов, создаваемых фабричным методом.
  2. ConcreteProduct – конкретный продукт. Реализует интерфейс Product.
  3. Creator – создатель. Объявляет фабричный метод, возвращающий объект типа Product.
  4. ConcreteCreator – конкретный создатель. Замещает фабричный метод, возвращающий объект ConcreteProduct.

Например, в .Net Framework абстрактный класс System.Data.Common.DBConnection имеет фабричный метод CreateCommand, который возвращает объект типа DBCommand. Наследники этого класса, System.Data.Odbc.OdbcConnection, System.Data.OleDB.OleDBConnection и System.Data.SqlClient.SqlConnection, перекрывают метод CreateCommand и возвращают экземпляры, специфичные для своего протокола команд общения с СУБД – OdbcCommand, OleDBCommand, SqlCommand. В терминах шаблона фабричный класс DBCommand является продуктом, OdbcCommand, OleDBCommand и OleDBCommand представляют собой продукты, DbConnection является создателем, а OdbcConnection, OleDBConnection и SqlConnection – конкретными создателями.

Среди различных реализаций данного паттерна выделяют параметризованные фабричные методы ([1], стр. 115). В фабричный метод передается ключ, на основании которого конкретный создатель решает, какой именно конкретный продукт создать и вернуть. Большой набор значений ключа часто приводит к загроможденному коду и, как следствие, к трудностям сопровождения приложения. Данную проблему можно устранить при помощи обобщений (generics) и анонимных методов (anonymous methods) языка C# 2.0. Для этого предлагается ввести дополнительный класс KeyedFactory, на который возлагается выбор метода создания объекта и его вызов. Конкретный создатель при инициализации регистрирует в KeyedFactory пары <значение, делегат создания> и непосредственно в фабричном методе делегирует создание объекта KeyedFactory. По сути своей KeyedFactory является словарем <ключ, фабричный метод>. Модифицированная структура шаблона Factory Method приведена на рисунке 2.


Рисунок 2. Модифицированная структура шаблона Factory Method.

Если перенести KeyedFactory в Creator и создать public-метод для регистрации продуктов по ключу, то необходимость в порождении ConcreteCreator отпадает, т.е. клиент сможет через метод Register изменить создаваемый тип продуктов непосредственно в создателе.

Рассмотрим более подробно реализацию класса KeyedFactory. В основе реализации лежит обобщенный делегат TProduct FactoryMethod<TProduct>(), экземпляры которого хранятся в качестве значений словаря <TKey, FactoryMethod<TProduct>> в private-поле _factoryMethods. В методе KeyedFactory.Create(TKey key) из словаря достается делегат, и с его помощью создается экземпляр продукта. В общем случае в KeyedFactory при помощи метода RegisterMethod регистрируются фабричные методы (через делегаты), в C# регистрация выглядит следующим образом:

factory.RegisterMethod(keyValue, 
new FactoryMethod<TProduct>(SomeClass.ProductFactoryMethod));

Компилятор C# 2.0 понимает более компактную запись, без явного создания делегата:

factory.RegisterMethod(keyValue, SomeClass.ProductFactoryMethod);

Также возможно использование анонимных методов:

factory.RegisterMethod(keyValue, delegate { returnnew ConcreteProduct(); });

При помощи лямбда-выражений C# 3.0 эту запись можно сократить:

factory.RegisterMethod(keyValue, () => new ConcreteProduct());

Если конкретный продукт имеет public-конструктор без параметров, то создание делегата для удобства можно перенести в саму фабрику, используя ограничитель (constrain) обобщения new() следующим образом:

      public
      void RegisterType<TConcreteProduct>(TKey key)
            where TConcreteProduct: TProduct, new()
{
     RegisterMethod(key, delegate { returnnew TConcreteProduct(); });
}

Более того, функциональность KeyedFactory можно расширить поддержкой прототипов. Для этого используется способность анонимных методов захватывать внешние переменные – замыкание (closure). До тех пор, пока где-то имеется «живая» ссылка на делегат, который, в свою очередь ссылается на анонимный метод, будут живы все переменные, на которые замкнут этот анонимный метод. Физически в текущих версиях компилятора C# от Microsoft для этого используется генерирование скрытого класса, но это уже детали реализации. Этой возможностью предлагается воспользоваться для реализации поддержки прототипов в KeyedFactory.

      public
      void RegisterPrototype<TConcreteProduct>(
TKey key, 
  TConcreteProduct prototype
)
  where  TConcreteProduct: TProduct, ICloneable
{
  RegisterMethod(key, delegate { return (TProduct) prototype.Clone(); });
}

Так как регистрируемый делегат сохраняется в словаре, то замыкание (а стало быть и ссылки на все захваченные им объекты) будет жить, пока делегат не будет удален из словаря, или на словарь не останется ссылок. Таким образом, при регистрации продукта создающий его код (опосредованно через замыкание) сохранится в поле _factoryMethods. Такой же подход можно применить и для создания объектов, которые имеют неизменяемое состояние (value-objects). Поддержка value-objects реализована в методе RegisterImmutable.

Однако при такой регистрации есть один недостаток. Так как фабричный метод должен вызывать метод Clone() объекта-прототипа, этот объект должен быть создан до первого вызова фабричного метода. В принципе, несложно написать код, который проверял бы, создан ли прототип и, если нужно, создавал его прямо внутри делегата. Но зачем делать что-то многократно, если можно сделать это один раз? Для упрощения поддержки ленивого создания прототипов было придумано следующее: использование фабричных методов – «заглушек». Задача этих методов – создать прототип во время первого вызова, то есть регистрируется фабричный метод, в коде которого непосредственно создается прототип, и возвращается делегат, который, в свою очередь, возвращает копии этого прототипа. При первом вызове первого делегата ссылка (в _factoryMethods) на него перезаписывается ссылкой на второй делегат, возвращающий копии прототипа. В следующий раз при вызове Create с этим же ключом вызовется уже второй фабричный метод. Поддержка этого подхода для прототипов и value-object находится в методах RegisterLazyPrototype и RegisterLazyImmutable соответственно.

Время создания объектов

Если требуется очень часто создавать объекты, например, при выстраивании большого дерева (например, AST), время создания объектов может оказаться критичным. Для определения времени создания объектов был проведен ряд экспериментов. В каждом эксперименте создавались продукты ссылочного и размерного типов с разными модификаторами доступа (public, internal). В тесте используются следующие способы создания объектов:

Вызов оператора new T().

Вызов метода Activator.CreateInstance<T>().

Вызов метода Activator.CreateInstance(Type).

Вызов оператора new.

Возврат существующего объекта.

Вызов метода prototype.Clone().

Вызов метода ConstructorInfo.Invoke().

Производился замер времени создания N объектов (N = 1000000) каждым способом. Все тесты проводились на компьютере Intel Core2 CPU T7400 2.17GHz, 2046 MB RAM, Windows Vista Ultimate 32-bit, .Net v2.0.50727.1378. Результаты 10 экспериментов сведены в следующую таблицу:

Способ создания объекта Среднее время создания объектов public-классов (мс) Среднее время создания объектов private-классов (мс) Среднее время создания объектов public-структур (мс) Среднее время создания объектов private-структур (мс)
вызов оператора new T() 1605,70 1574,00 93,40 94,90
вызов Activator.CreateInstance<T>() 1494,70 1457,90 274,60 280,00
вызов Activator.CreateInstance(Type) 246,00 2623,60 188,40 3121,50
вызов оператора new 53,50 46,40 48,70 50,30
возврат существующего объекта 44,90 38,10 38,00 38,30
вызов prototype.Clone() 222,60 221,80 89,00 89,60
вызов ConstructorInfo.Invoke() 1213,20 3614,70 Нет Нет

Из полученных результатов видно, что время создания объекта при помощи Activator.CreateInstance(Type) и ConstructorInfo.Invoke() сильно зависит от модификатора доступа типа – при создании объектов public-типов эти способы показывают вполне удовлетворительное время, но при создании private-типов они показывают наихудшие результаты. Далее отметим, что разница между new T() и Activator.CreateInstance<T>() незначительна в случае создания объектов классов и, наоборот, в случае создания объектов структур время отличается в разы. Если посмотреть сгенерированный для new T() MSIL-код, то можно обнаружить примерно следующее:

T CreateViaNew<T>() 
{
  T result = default(T); // инициализируем переменную нулямиif (box(result) != null) // в случае размерных типов будет falsereturn result;
  elsereturn Activator.CreateInstance<T>();
}

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

Выводы

Описанный подход к реализации Parameterized Factory Method делает код более простым и понятным. KeyedFactory представляет удобные методы регистрации способов создания объектов: RegisterType, RegisterPrototype, RegisterImmutable, RegisterLazyPrototype, RegisterLazyImmutable. Однако, как видно, способ создания объектов сильно влияет на производительность и разработчик должен это учитывать при его использовании.

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

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

  1. Гамма Э, и др. Примеры объектного-ориентированного проектирования. Паттерны проектирования. [перев.] А. Слинкин. Спб.: Питер, 2006. стр. 366. ISBN 5-272-00355-1.
  2. Бек К. Экстремальное программирование: разработка через тестирование. Библиотека программиста. – СПб.: Питер 2003. стр. 224. ISBN 5-8046-0051-6
  3. Кериевски, Джошуа. Рефакторинг с использованием шаблонов.: Пер. с англ. – М.: ООО «И.Д. Вильямс», 2006.- 400с.: ил. – Парал.тит.англ. ISBN 5-8459-1087-0
  4. Grady Booch, It Is What It Is Because It Was What It Was, IEEE SOFTWARE, January/February 2007, IEEE Computer Society, 2007

Эта статья опубликована в журнале RSDN Magazine #1-2008. Информацию о журнале можно найти здесь
    Сообщений 2    Оценка 71 [+0/-1]         Оценить