Порождающие шаблоны: Фабричный метод (Factory Method)

Название шаблона

Фабричный метод (Factory Method).

Тип

Порождающий шаблон проектирования (Creational).

Описание

Factory Method patternФабричный метод применяется для создания объектов с определенным интерфейсом, реализации которого предоставляются потомками.

Шаблон используется в случаях если:

  • класс заранее не знает, какие объекты необходимо будет создавать, т.к. возможны варианты реализации;
  • (или) класс спроектирован так, что спецификация порождаемого объекта определяется только в наследниках.
  • (или) класс выделяет и делегирует часть своих функций вспомогательному классу. При этом необходимо скрыть его реализацию для достижения большей гибкости или возможности расширения функциональности.

Схожие шаблоны и их отличия

Фабричный метод Абстрактная фабрика Строитель
Порождает один объект с определенным интерфейсом. Порождает семейство объектов с определенными интерфейсами. Создает в несколько шагов один сложный (составной) объект.
Метод класса, который переопределяется потомками. Интерфейс, реализуемый классами. Интерфейс строителя, реализуемый классами, и класс для управления процессом.
Скрывает реализацию объекта. Скрывает реализацию семейства объектов. Скрывает процесс создания объекта, порождает требуемую реализацию.

Реализация шаблона в общем виде

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

Комментарии (12) -

Очень хорошо! Давно искал эту(по шаблонам) информацию. Мой уровень C# несколько ниже уровня этой статьи, кое-что осталось не понятным, но все же продолжайте писать! Лично меня эта тема очень интересует.

@ DmitryBLR: Спасибо. Но интересно, что именно не понятно?

Скажите, пожалуйста, в какой программе Вы рисовали диаграмму? Уж больно она красивая получилась Smile

@ Alex: Microsoft PowerPoint 2010 Smile

То, что не понятно мне, это не Ваш "недочет", это мои пробелы. Про generics не понятно. В том смысле, что я вообще не знаю, что это такое.

sombre hombre 14.10.2010 14:21:57

Пример использования generic-варианта, по-моему, очень неудачный. Зачем вообще нужен DocumentManager, если мы и так указываем конкретные классы при вызове CreateStorage?

@ sombre hombre: согласен, поправил пример.

Статья отличная, единственное неудобно то, что диаграмму нормально можно посмотреть только открыв новую страницу.

Денис 08.09.2011 19:20:12

Ура!!! Я наконец то понял!!!

Андрей можно вопросик пожалуйста. Не совсем понятен последний паттерн Generic как я понял за место создания двух классов наследников мы используем обобщение.

public class DocumentManagerGeneric<T> : DocumentManager where T : IDocStorage, new()
  { public override IDocStorage CreateStorage()...
вопрос собственно почему  public override IDocStorage? его же надо сначала abstract в базовом классе. И еще вопросик наследование можно делать в методах
public void SetStorage<T>() where T : IDocStorage, new()

private IDocStorage _storage; - IDocStorage определиться? я просто не знаю.Извините я скорее всего не улавливаю многого. Тоже недавно изучаю C# пока только хобби.

public void SetStorage<T>() where T : IDocStorage, new()
private IDocStorage _storage; - IDocStorage определиться? --- глупый вопрос извининте плиз. По поводу  "public override IDocStorage" пока что не понял.

@ Alex: Базовый класс - DocumentManager. В нем определен метод CreateStorage(), кстати абстрактный. Тут ты определяем его конкретную реализацию.

Добавить комментарий