Название шаблона
Фабричный метод (Factory Method).
Тип
Порождающий шаблон проектирования (Creational).
Описание
Фабричный метод применяется для создания объектов с определенным интерфейсом, реализации которого предоставляются потомками.
Шаблон используется в случаях если:
- класс заранее не знает, какие объекты необходимо будет создавать, т.к. возможны варианты реализации;
- (или) класс спроектирован так, что спецификация порождаемого объекта определяется только в наследниках.
- (или) класс выделяет и делегирует часть своих функций вспомогательному классу. При этом необходимо скрыть его реализацию для достижения большей гибкости или возможности расширения функциональности.
Схожие шаблоны и их отличия
Фабричный метод | Абстрактная фабрика | Строитель |
Порождает один объект с определенным интерфейсом. | Порождает семейство объектов с определенными интерфейсами. | Создает в несколько шагов один сложный (составной) объект. |
Метод класса, который переопределяется потомками. | Интерфейс, реализуемый классами. | Интерфейс строителя, реализуемый классами, и класс для управления процессом. |
Скрывает реализацию объекта. | Скрывает реализацию семейства объектов. | Скрывает процесс создания объекта, порождает требуемую реализацию. |
Реализация шаблона в общем виде
- определяется интерфейс порождаемых объектов IProduct;
- базовый класс описывает метод public IProduct FabricMethod() для их создания;
- наследники переопределяют его, порождая свои реализации IProduct;
- базовый класс и клиентский код используют в работе только интерфейс IProduct, не обращаясь к конкретным реализациям самостоятельно.
Примеры реализации
1. Абстрактный метод или метод из интерфейса
Данный подход обязывает потомка определить свои реализации Фабричного метода и порождаемого им класса.
Рассмотрим на примере класса DocumentManager, отвечающего за работу с документом. Вынесем функции работы с хранилищем, сохранение и загрузку документа, в отдельный интерфейс IDocStorage.
public interface IDocStorage
{
void Save(string name, Document document);
Document Load(string name);
}
В классе DocumentManager добавим абстрактный Фабричный метод CreateStorage() для создания нового хранилища. И, ��ля примера его использования, напишем метод Save(), сохраняющий документ.
public abstract class DocumentManager
{
public abstract IDocStorage CreateStorage();
public bool Save(Document document)
{
if (!this.SaveDialog()) {
return false;
}
// using Factory method to create a new document storage
IDocStorage storage = this.CreateStorage();
storage.Save(this._name, document);
return true;
}
}
Определим потомки класса DocumentManager, которые будут сохранять документы в txt и rtf форматах. Реализации IDocStorage разместим в вложенных private классах. Это обеспечит нужный уровень абстракции хранилища, позволив клиентскому коду работать с ними только через интерфейс.
Для краткости, у классов TxtDocStorage и RtfDocStorage убран код их методов.
public class TxtDocumentManager : DocumentManager
{
private class TxtDocStorage : IDocStorage { }
public override IDocStorage CreateStorage() { return new TxtDocStorage(); }
}
public class RtfDocumentManager : DocumentManager
{
private class RtfDocStorage : IDocStorage { }
public override IDocStorage CreateStorage() { return new RtfDocStorage(); }
}
Теперь результат вызова метода DocumentManager.CreateStorage() будет экземпляром TxtDocStorage или RtfDocStorage. Это будет определяться в зависимости от того, какой потомок абстрактного класса был создан. Значит вызов метода DocumentManager.Save() сохранит данные в соответствующем формате.
// Save a document as txt file using "Save" dialog
DocumentManager docManager = new TxtDocumentManager();
docManager.Save(document);
// Or use the IDocStorage interface to save a document
IDocStorage storage = docManager.CreateStorage();
storage.Save(name, newDocument);
2. Метод класса
Данный подход почти аналогичен рассмотренному выше варианту. Единственное отличие заключается в том, что базовый класс содержит реализации метода CreateStorage() и интерфейса IDocStorage. Потомки могут как использовать их, так и переопределить, если необходимо изменить функциональность.
3. Параметризованный метод
Частный случай Фабричного метода. Входной параметр используется для определения, какую реализацию интерфейса требуется создать:
public enum StorageFormat { Txt, Rtf }
public IDocStorage CreateStorage(StorageFormat format)
{
switch (format) {
case StorageFormat.Txt:
return new TxtDocStorage();
case StorageFormat.Rtf:
return new RtfDocStorage();
default:
throw new ArgumentException("An invalid format: " + format.ToString());
}
}
4. Использование generics (общих типов/шаблонов)
Еще один частный случай – использование generics для создания потомков классов. В некоторых случаях это может полностью заменить создание наследников вручную. Например, когда код методов отличается только порождаемым классом.
В C# есть хорошая возможность ограничить типы, используемые в качестве параметра generics, используя ключевое слово where. Так, для класса DocumentManagerGeneric будем требовать наличие IDocStorage и public конструктора без параметров.
Теперь создадим generic-класс, унаследовав его от DocumentManager:
public class DocumentManagerGeneric<T> : DocumentManager where T : IDocStorage, new()
{
public override IDocStorage CreateStorage()
{
IDocStorage storage = new T();
// TODO: Setup, test, or do something else with the storage, if required.
return storage;
}
}
При создании экземпляра этого класса, необходимо указать класс используемого хранилища:
DocumentManager docManager = new DocumentManagerGeneric<RtfDocStorage>();
В дальнейшем его экземпляр и будет использоваться в методе Save().
С некоторым допущением, но все же можно отнести к данному шаблону проектирования версию с generic-методом. Здесь нет наследования, но в момент разработки не известно, экземпляры каких классов необходимо будет порождать.
Создадим хранилище, требуемого типа, в метода SetStorage() и сохраним его в закрытом поле:
public class DocumentManager
{
private IDocStorage _storage;
public void SetStorage<T>() where T : IDocStorage, new()
{
this._storage = new T();
// TODO: Setup, test, or do something else with the storage, if required.
}
}
Сам тип становится известен только при разработке кода, использующего класс DocumentManager:
DocumentManager docManager2 = new DocumentManager();
docManager2.SetStorage<TxtDocStorage>();
docManager2.Save();
Возможно возникнет вопрос, почему просто не передавать хранилище как параметр? Однако, используемый вариант позволяет:
- вынести в метод SetStorage() не только создание, но и настройку экземпляра класса;
- выполнить проверку поддержки требуемого интерфейса IDocStorage на этапе компиляции;
- создать экземпляр класса хранилища только для внутреннего использования.
Таком образом уменьшается зависимость класса DocumentManager от внешнего кода и увеличивается контроль над экземпляром класса хранилища. Например, нет необходимости ожидать, что хранилище может быть закрыто клиентским кодом через свою переменную, указывающую на этот же экземпляр.