factory-methodФабричный метод (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 от внешнего кода и увеличивается контроль над экземпляром класса хранилища. Например, нет необходимости ожидать, что хранилище может быть закрыто клиентским кодом через свою переменную, указывающую на этот же экземпляр.