Введение
Статья Singleton показала что для контроля создания единственного экземпляра — Factory Method и Abstract Factory решают другую, но связанную задачу: инкапсуляцию ЛОГИКИ создания объектов, особенно когда конкретный тип создаваемого объекта должен определяться динамически, без жёсткого, явного указания конкретного класса в коде, который использует созданный объект.
Концепция
Factory Method определяет метод (обычно виртуальный), отвечающий за создание объекта определённого, абстрактного типа — конкретный класс создаваемого объекта определяется тем, какая ПРОИЗВОДНАЯ реализация фабричного метода вызывается, что позволяет легко добавлять новые типы создаваемых объектов через создание новой производной фабрики, без изменения существующего кода, использующего абстрактный фабричный интерфейс (принцип открытости/закрытости). Abstract Factory расширяет эту идею для создания целых СЕМЕЙСТВ связанных объектов (например, набор UI-элементов одного, согласованного визуального стиля) одним согласованным фабричным объектом, гарантируя, что все созданные через одну конкретную фабрику объекты принадлежат к одному, совместимому семейству.
Пример кода
#include <iostream>
#include <memory>
// Factory Method — абстрактный продукт и абстрактный создатель
class Document { public: virtual void render() const = 0; virtual ~Document() = default; };
class PdfDocument : public Document { public: void render() const override { std::cout << "Рендеринг PDF" << std::endl; } };
class WordDocument : public Document { public: void render() const override { std::cout << "Рендеринг Word" << std::endl; } };
class DocumentCreator
{
public:
virtual std::unique_ptr<Document> createDocument() const = 0; // ФАБРИЧНЫЙ метод — = 0
virtual ~DocumentCreator() = default;
void processDocument() const // использует фабричный метод, НЕ зная конкретный тип
{
auto doc = createDocument();
doc->render();
}
};
class PdfCreator : public DocumentCreator
{
public:
std::unique_ptr<Document> createDocument() const override { return std::make_unique<PdfDocument>(); }
};
int main()
{
PdfCreator creator;
creator.processDocument(); // "Рендеринг PDF" — конкретный тип скрыт за абстракцией DocumentCreator
return 0;
}
// Abstract Factory — создание СЕМЕЙСТВА связанных объектов (UI-стиль)
class Button { public: virtual void draw() const = 0; virtual ~Button() = default; };
class Checkbox { public: virtual void draw() const = 0; virtual ~Checkbox() = default; };
class DarkButton : public Button { public: void draw() const override { std::cout << "Тёмная кнопка" << std::endl; } };
class DarkCheckbox : public Checkbox { public: void draw() const override { std::cout << "Тёмный чекбокс" << std::endl; } };
class UIFactory // Abstract Factory — гарантирует СОГЛАСОВАННОСТЬ всего набора создаваемых элементов
{
public:
virtual std::unique_ptr<Button> createButton() const = 0;
virtual std::unique_ptr<Checkbox> createCheckbox() const = 0;
virtual ~UIFactory() = default;
};
class DarkThemeFactory : public UIFactory
{
public:
std::unique_ptr<Button> createButton() const override { return std::make_unique<DarkButton>(); }
std::unique_ptr<Checkbox> createCheckbox() const override { return std::make_unique<DarkCheckbox>(); }
};
Пояснения к коду
DocumentCreator::processDocument() показывает суть Factory Method — этот метод вызывает createDocument(), не зная, какой конкретный тип Document реально будет создан (это определяется тем, какая конкретная производная фабрика, PdfCreator или потенциальная WordCreator, реально используется), что позволяет добавлять новые типы документов без изменения логики processDocument(). UIFactory с методами createButton()/createCheckbox() показывает расширение идеи до целого семейства связанных продуктов — DarkThemeFactory гарантирует, что и кнопка, и чекбокс, созданные через ЭТУ конкретную фабрику, будут визуально согласованы (оба в тёмной теме), что значительно сложнее гарантировать, если бы кнопки и чекбоксы создавались независимо, без объединяющей их фабрики семейства.
Подводные камни
- Применение Factory Method/Abstract Factory для единственного, никогда не меняющегося типа создаваемого объекта — если в проекте реально существует и предвидится только один тип
Document(толькоPdfDocument, без реальной перспективы добавленияWordDocument), вся инфраструктура абстрактной фабрики добавляет сложность без какой-либо практической выгоды по сравнению с прямым, непосредственным созданием объекта конкретного типа — паттерн оправдан именно реальной, обоснованной потребностью в полиморфном выборе типа создаваемого объекта. - Создание глубокой иерархии фабрик для случаев, где достаточно более простого механизма (как обычный
if/switchдля выбора между небольшим, заранее известным и редко изменяющимся набором типов, статья цикла синтаксиса) — Factory Method особенно ценен, когда конкретные подклассы фабрики могут быть добавлены независимо, во внешнем, неизвестном заранее коде (как плагины), но для небольшого, контролируемого набора альтернатив в пределах одного проекта более простое, прямое условное создание может быть достаточным и более понятным решением. - Игнорирование владения и времени жизни созданных через фабрику объектов, особенно при использовании «сырых» указателей вместо умных указателей — фабричный метод, возвращающий «сырой» указатель без явной, документированной договорённости о том, кто отвечает за последующее освобождение этого объекта, создаёт риск утечки памяти или двойного освобождения, и современная практика (как показано в примерах через
std::unique_ptr) явно решает этот вопрос через систему типов, делая владение однозначным и очевидным. - Путаница между Factory Method (создание ОДНОГО продукта через виртуальный метод, переопределяемый в подклассах) и просто статической фабричной функцией без полиморфизма (
static std::unique_ptr<Document> createDocument(DocumentType type), выбирающей конкретный тип через параметр, а не через переопределение в подклассе) — оба подхода решают похожую внешнюю задачу (инкапсуляция логики создания), но второй вариант (часто называемый просто «static factory method», не идентичный классическому GoF Factory Method паттерну) не даёт той же гибкости расширения через наследование, что важно различать при выборе подходящего конкретного решения для задачи.