Card 04/ 09
Builder Example: A Report With Optional Sections
A title is required; a chart, a summary and a footer are each optional.
A report generator is a natural Builder: every report has a title, and any report may add a summary, a chart and a footer. This example maps the template onto it, and shows the builder used both as a chain and across several statements.
| Participant | Class in this example |
|---|---|
| ConcreteBuilder | ReportBuilder, with title, summary, chart and footer |
| Product | Report, a frozen dataclass with render() |
| Builder | None: with one way to build a report, there is no interface |
| Director | None; the calling code chooses the steps |
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class Report:
title: str
summary: Optional[str]
chart: Optional[str]
footer: Optional[str]
def render(self) -> str:
lines = [f"# {self.title}"]
if self.summary:
lines.append(self.summary)
if self.chart:
lines.append(f"[chart: {self.chart}]")
if self.footer:
lines.append(f"-- {self.footer}")
return "\n".join(lines)
class ReportBuilder:
def __init__(self) -> None:
self._title: Optional[str] = None
self._summary: Optional[str] = None
self._chart: Optional[str] = None
self._footer: Optional[str] = None
def title(self, text: str) -> "ReportBuilder":
self._title = text
return self
def summary(self, text: str) -> "ReportBuilder":
self._summary = text
return self
def chart(self, data_name: str) -> "ReportBuilder":
self._chart = data_name
return self
def footer(self, text: str) -> "ReportBuilder":
self._footer = text
return self
def build(self) -> Report:
if not self._title:
raise ValueError("a report needs a title")
return Report(self._title, self._summary, self._chart, self._footer)
weekly = (
ReportBuilder()
.title("Weekly sales")
.summary("Orders rose 12% on last week.")
.chart("orders_by_day")
.footer("Generated on Monday")
.build()
)
print(weekly.render())
print()
builder = ReportBuilder().title("Stock check")
low_stock = ["kettle", "toaster"]
if low_stock:
builder.summary("Low: " + ", ".join(low_stock))
print(builder.build().render())
print()
try:
ReportBuilder().chart("orders_by_day").build()
except ValueError as error:
print("refused:", error)# Weekly sales
Orders rose 12% on last week.
[chart: orders_by_day]
-- Generated on Monday
# Stock check
Low: kettle, toaster
refused: a report needs a titleThe weekly report is built in one chain, and every optional part is named where it is set. The stock check shows the case a chain cannot: the builder is created first, the summary is added only if the condition holds, and build() is called once everything that applies has been decided. The third call sets a chart and no title, and build() refuses it.
The builder here has no interface and no director, which is the form most builders take: one concrete builder that the client drives directly. The participants that remain, a builder that collects parts and a build() that checks them, are the ones that do the work.
← → move through sections and cards