Card 01/ 05

ComparisonDifficulty: Intermediate3 min

Factory Method vs Abstract Factory vs Builder

One product chosen by a subclass, a family chosen by a factory object, or one product assembled in steps.

Factory Method, Abstract Factory and Builder all move the creation of an object out of the code that uses it, and they differ in what varies. With Factory Method the class of one product varies, chosen by a subclass. With Abstract Factory a whole family of products varies together, chosen by a factory object. With Builder the parts of one product vary, assembled step by step. This card compares the three and then solves one problem each way.

What each pattern lets vary
PatternWhat variesWho decidesA new variant costs
Factory MethodThe class of one productA subclass of the creatorOne creator subclass and one product class
Abstract FactoryA family of related productsThe factory object the client is givenOne factory class and one class per product kind
BuilderThe parts and order of one complex productThe client or a director calling the stepsNothing, for a new combination of existing steps

The last column is the quickest test. A new kind of output under Factory Method is a pair of classes. Under Abstract Factory it is a class for every product in the family, plus the factory. Under Builder, a report with an extra table is not a new class at all, only another call.

The problem below is a report exported as PDF or HTML. When the export needs one object, a writer, and the format decides its class, it is Factory Method.

py
from abc import ABC, abstractmethod


class Writer(ABC):
    @abstractmethod
    def write(self, title: str, body: str) -> str: ...


class PdfWriter(Writer):
    def write(self, title: str, body: str) -> str:
        return f"%PDF [{title}] {body}"


class HtmlWriter(Writer):
    def write(self, title: str, body: str) -> str:
        return f"<h1>{title}</h1><p>{body}</p>"


class Exporter(ABC):
    @abstractmethod
    def create_writer(self) -> Writer: ...

    def export(self, title: str, body: str) -> str:
        return self.create_writer().write(title, body)


class PdfExporter(Exporter):
    def create_writer(self) -> Writer:
        return PdfWriter()


class HtmlExporter(Exporter):
    def create_writer(self) -> Writer:
        return HtmlWriter()


for exporter in [PdfExporter(), HtmlExporter()]:
    print(exporter.export("Q3 report", "Sales rose."))
OutputRan with python:3.12-slim (Python 3.12.13)
%PDF [Q3 report] Sales rose.
<h1>Q3 report</h1><p>Sales rose.</p>
Factory Method: each exporter subclass overrides `create_writer`.

When the export needs two objects that must match, a writer and its styles, and mixing a PDF writer with HTML styles would be a bug, it is Abstract Factory. One kit object creates both, so the client cannot pick them separately.

py
from abc import ABC, abstractmethod


class Writer(ABC):
    @abstractmethod
    def write(self, title: str, body: str) -> str: ...


class Styles(ABC):
    @abstractmethod
    def apply(self, text: str) -> str: ...


class PdfWriter(Writer):
    def write(self, title: str, body: str) -> str:
        return f"%PDF [{title}] {body}"


class PdfStyles(Styles):
    def apply(self, text: str) -> str:
        return f"{text} /Font Helvetica"


class HtmlWriter(Writer):
    def write(self, title: str, body: str) -> str:
        return f"<h1>{title}</h1><p>{body}</p>"


class HtmlStyles(Styles):
    def apply(self, text: str) -> str:
        return f'<link rel="stylesheet" href="report.css">{text}'


class ExportKit(ABC):
    @abstractmethod
    def create_writer(self) -> Writer: ...

    @abstractmethod
    def create_styles(self) -> Styles: ...


class PdfKit(ExportKit):
    def create_writer(self) -> Writer:
        return PdfWriter()

    def create_styles(self) -> Styles:
        return PdfStyles()


class HtmlKit(ExportKit):
    def create_writer(self) -> Writer:
        return HtmlWriter()

    def create_styles(self) -> Styles:
        return HtmlStyles()


def export(kit: ExportKit, title: str, body: str) -> str:
    return kit.create_styles().apply(kit.create_writer().write(title, body))


for kit in [PdfKit(), HtmlKit()]:
    print(export(kit, "Q3 report", "Sales rose."))
OutputRan with python:3.12-slim (Python 3.12.13)
%PDF [Q3 report] Sales rose. /Font Helvetica
<link rel="stylesheet" href="report.css"><h1>Q3 report</h1><p>Sales rose.</p>
Abstract Factory: one kit creates the writer and the styles that go with it.

When there is one format but the report itself varies, a heading always, then paragraphs, then a table only when there is data, it is Builder. The variety is in which steps run, not in which class is created.

py
class HtmlReportBuilder:
    def __init__(self) -> None:
        self._parts: list[str] = []

    def heading(self, text: str) -> "HtmlReportBuilder":
        self._parts.append(f"<h1>{text}</h1>")
        return self

    def paragraph(self, text: str) -> "HtmlReportBuilder":
        self._parts.append(f"<p>{text}</p>")
        return self

    def table(self, rows: list[tuple[str, int]]) -> "HtmlReportBuilder":
        cells = "".join(f"<tr><td>{name}</td><td>{value}</td></tr>" for name, value in rows)
        self._parts.append(f"<table>{cells}</table>")
        return self

    def build(self) -> str:
        if not self._parts or not self._parts[0].startswith("<h1>"):
            raise ValueError("a report starts with a heading")
        return "".join(self._parts)


report = HtmlReportBuilder().heading("Q3 report").paragraph("Sales rose.")
regions = [("North", 120), ("South", 95)]
if regions:
    report.table(regions)
print(report.build())
OutputRan with python:3.12-slim (Python 3.12.13)
<h1>Q3 report</h1><p>Sales rose.</p><table><tr><td>North</td><td>120</td></tr><tr><td>South</td><td>95</td></tr></table>
Builder: the table step runs only when there are regions to show.

The three can be combined, and in real code often are: an Abstract Factory whose PDF kit returns a builder for PDF reports. Asking what varies separates them each time, because each pattern answers a different version of that question.

← → move through sections and cards