Abstract Factory Pattern


  • Description: Create families of related objects without naming concrete classes — one factory object per family variant; covers classic GoF form, modern template/policy alternatives, and the trade-off vs Factory Method.
  • My Notion Note ID: K2C-2-3
  • Created: 2026-05-22
  • Updated: 2026-05-22
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Why It Exists

  • Many systems need multiple related objects produced together that must agree on a variant — e.g. a UI toolkit's Button + Checkbox + Window must all be the macOS variants, or all the Windows variants. Mixing is a bug.
  • Direct construction (new MacButton, new MacCheckbox) scatters the variant decision across the codebase.
  • Abstract Factory bundles the family behind one factory interface → swap the entire variant by swapping the factory.

2. Structure

Role Responsibility
AbstractFactory Interface with one method per product type in the family.
ConcreteFactory Implements AbstractFactory for one variant (e.g. Mac, Windows).
AbstractProduct (one per slot) Interface for each kind of product (Button, Checkbox).
ConcreteProduct Variant-specific implementation.
Client Talks only to AbstractFactory + AbstractProduct — never names concretes.
  • Factory object (composition), not factory method (inheritance). Each method on the factory is a Factory Method in the GoF sense.

3. C++ Implementations

3.1 Classic GoF form

#include <memory>
#include <iostream>

struct Button   { virtual ~Button()   = default; virtual void paint() = 0; };
struct Checkbox { virtual ~Checkbox() = default; virtual void paint() = 0; };

struct MacButton   : Button   { void paint() override { std::cout << "[Mac Button]\n"; } };
struct MacCheckbox : Checkbox { void paint() override { std::cout << "[Mac Check]\n"; } };

struct WinButton   : Button   { void paint() override { std::cout << "[Win Button]\n"; } };
struct WinCheckbox : Checkbox { void paint() override { std::cout << "[Win Check]\n"; } };

struct GuiFactory {
    virtual ~GuiFactory() = default;
    virtual std::unique_ptr<Button>   createButton()   = 0;
    virtual std::unique_ptr<Checkbox> createCheckbox() = 0;
};

struct MacFactory : GuiFactory {
    std::unique_ptr<Button>   createButton()   override { return std::make_unique<MacButton>(); }
    std::unique_ptr<Checkbox> createCheckbox() override { return std::make_unique<MacCheckbox>(); }
};

struct WinFactory : GuiFactory {
    std::unique_ptr<Button>   createButton()   override { return std::make_unique<WinButton>(); }
    std::unique_ptr<Checkbox> createCheckbox() override { return std::make_unique<WinCheckbox>(); }
};

void buildDialog(GuiFactory& f) {
    auto btn = f.createButton();
    auto chk = f.createCheckbox();
    btn->paint();
    chk->paint();
}
  • Add a Linux variant → add LinuxButton, LinuxCheckbox, LinuxFactory. No edits to buildDialog or GuiFactory.
  • Add a new product slot (e.g. Slider) → edit GuiFactory + every concrete factory. The pattern is closed for variants, open for products — adding products is expensive.

3.2 Modern alternative — template policy

template <class ButtonT, class CheckboxT>
struct GuiToolkit {
    ButtonT   makeButton()   const { return {}; }
    CheckboxT makeCheckbox() const { return {}; }
};

using MacToolkit = GuiToolkit<MacButton, MacCheckbox>;
using WinToolkit = GuiToolkit<WinButton, WinCheckbox>;

template <class Toolkit>
void buildDialog(const Toolkit& t) {
    auto btn = t.makeButton();
    auto chk = t.makeCheckbox();
    btn.paint();
    chk.paint();
}
  • Zero virtual dispatch; full inlining. Variant fixed per translation unit / template instantiation — can't pick at runtime.

3.3 Modern alternative — std::variant of factories

#include <variant>

struct MacFactoryV { /* methods returning concrete products */ };
struct WinFactoryV { /* ... */ };

using AnyFactory = std::variant<MacFactoryV, WinFactoryV>;

void buildDialog(AnyFactory& f) {
    std::visit([](auto& factory) {
        auto btn = factory.createButton();
        auto chk = factory.createCheckbox();
        btn.paint(); chk.paint();
    }, f);
}
  • Trades extensibility (closed set) for compile-time exhaustiveness + no heap allocation.

4. When to Use, When Not To

Use when:

  • The system must work with multiple variants of a product family, picked at runtime or per-deployment.
  • Family-internal consistency matters — e.g. all UI widgets must match the host OS.
  • You want a single point of swap for the whole family (config-driven, test doubles).

Avoid when:

  • Only one variant exists — overengineered.
  • The "family" has one product → use Factory Method instead.
  • Products are added often, variants rarely — the pattern punishes that direction (every new product touches every factory).

5. Pitfalls

  • Adding a product is O(variants). Each new product method must be implemented on every concrete factory. If product set is volatile, abstract factory becomes a maintenance tax.
  • Returning raw pointers → ownership unclear. Always unique_ptr<Product> from factories.
  • Hidden coupling to factory choice — clients receive products as base types, but the products may need to interop and rely on being from the same variant. Document the invariant; don't mix factories.
  • Stateful factories — factories often look "naturally stateless" but real ones cache or pool. Then they're effectively singletons.
  • Confused with Builder — Abstract Factory builds many independent objects of a family. Builder builds one complex object step by step.

6. Related Patterns

  • Abstract Factory vs Factory Method — Factory Method = one product chosen via subclass override. Abstract Factory = a family of products chosen via factory object composition. Abstract Factory often uses Factory Method to implement each of its methods.
  • Abstract Factory vs Builder — Abstract Factory returns finished products immediately; Builder accumulates state and returns at the end via build().
  • Abstract Factory vs Prototype — Abstract Factory can be implemented as a Prototype registry: factory holds prototypical instances and clone()s them per call (Prototype Factory). Useful when product configuration is data-driven.
  • Abstract Factory vs Service Locator — Locator returns any service; Abstract Factory returns one family of related products. Locator more general, less type-safe.
  • Abstract Factory vs DI container — A DI container that resolves a whole graph by variant essentially is an abstract factory at scale.

7. References