Card 01/ 05
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.
| Pattern | What varies | Who decides | A new variant costs |
|---|---|---|---|
| Factory Method | The class of one product | A subclass of the creator | One creator subclass and one product class |
| Abstract Factory | A family of related products | The factory object the client is given | One factory class and one class per product kind |
| Builder | The parts and order of one complex product | The client or a director calling the steps | Nothing, 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.
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."))%PDF [Q3 report] Sales rose.
<h1>Q3 report</h1><p>Sales rose.</p>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.
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."))%PDF [Q3 report] Sales rose. /Font Helvetica
<link rel="stylesheet" href="report.css"><h1>Q3 report</h1><p>Sales rose.</p>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.
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())<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>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