Фабричный метод (Factory Method) – это паттерн, который определяет интерфейс для создания объектов некоторого класса, но непосредственное решение о том, объект какого класса создавать происходит в подклассах. То есть паттерн предполагает, что базовый класс делегирует создание объектов классам-наследникам.
Когда надо применять паттерн
- Когда заранее неизвестно, объекты каких типов необходимо создавать
- Когда система должна быть независимой от процесса создания новых объектов и расширяемой: в нее можно легко вводить новые классы, объекты которых система должна создавать.
- Когда создание новых объектов необходимо делегировать из базового класса классам наследникам
Формальное определение паттерна на языке C# может выглядеть следующим образом:
abstract class Product
{}
class ConcreteProductA : Product
{}
class ConcreteProductB : Product
{}
abstract class Creator
{
public abstract Product FactoryMethod();
}
class ConcreteCreatorA : Creator
{
public override Product FactoryMethod() { return new ConcreteProductA(); }
}
class ConcreteCreatorB : Creator
{
public override Product FactoryMethod() { return new ConcreteProductB(); }
}
Участники
- Абстрактный класс Product определяет интерфейс класса, объекты которого надо создавать.
- Конкретные классы ConcreteProductA и ConcreteProductB представляют реализацию класса Product. Таких классов может быть множество
- Абстрактный класс Creator определяет абстрактный фабричный метод
FactoryMethod(), который возвращает объект Product. - Конкретные классы ConcreteCreatorA и ConcreteCreatorB – наследники класса Creator, определяющие свою реализацию метода
FactoryMethod(). Причем методFactoryMethod()каждого отдельного класса-создателя возвращает определенный конкретный тип продукта. Для каждого конкретного класса продукта определяется свой конкретный класс создателя.Таким образом, класс Creator делегирует создание объекта Product своим наследникам. А классы ConcreteCreatorA и ConcreteCreatorB могут самостоятельно выбирать какой конкретный тип продукта им создавать.
Теперь рассмотрим на реальном примере. Допустим, мы создаем программу для сферы строительства. Возможно, вначале мы захотим построить многоэтажный панельный дом. И для этого выбирается соответствующий подрядчик, который возводит каменные дома. Затем нам захочется построить деревянный дом и для этого также надо будет выбрать нужного подрядчика:
class Program
{
static void Main(string[] args)
{
Developer dev = new PanelDeveloper("ООО КирпичСтрой");
House house2 = dev.Create();
dev = new WoodDeveloper("Частный застройщик");
House house = dev.Create();
Console.ReadLine();
}
}
// абстрактный класс строительной компании
abstract class Developer
{
public string Name { get; set; }
public Developer (string n)
{
Name = n;
}
// фабричный метод
abstract public House Create();
}
// строит панельные дома
class PanelDeveloper : Developer
{
public PanelDeveloper(string n) : base(n)
{ }
public override House Create()
{
return new PanelHouse();
}
}
// строит деревянные дома
class WoodDeveloper : Developer
{
public WoodDeveloper(string n) : base(n)
{ }
public override House Create()
{
return new WoodHouse();
}
}
abstract class House
{ }
class PanelHouse : House
{
public PanelHouse()
{
Console.WriteLine("Панельный дом построен");
}
}
class WoodHouse : House
{
public WoodHouse()
{
Console.WriteLine("Деревянный дом построен");
}
}
В качестве абстрактного класса Product здесь выступает класс House. Его две конкретные реализации – PanelHouse и WoodHouse представляют типы домов, которые будут строить подрядчики. В качестве абстрактного класса создателя выступает Developer, определяющий абстрактный метод Create(). Этот метод реализуется в классах-наследниках WoodDeveloper и PanelDeveloper. И если в будущем нам потребуется построить дома какого-то другого типа, например, кирпичные, то мы можем с легкостью создать новый класс кирпичных домов, унаследованный от House, и определить класс соответствующего подрядчика. Таким образом, система получится легко расширяемой. Правда, недостатки паттерна тоже очевидны – для каждого нового продукта необходимо создавать свой класс создателя.
Реализация шаблона в общем виде
- определяется интерфейс порождаемых объектов 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 от внешнего кода и увеличивается контроль над экземпляром класса хранилища. Например, нет необходимости ожидать, что хранилище может быть закрыто клиентским кодом через свою переменную, указывающую на этот же экземпляр.