Notes and exercises for learning design patterns
This solution uses builder inheritance for a small hierarchy:
Message
└── EmailMessage
And the builders mirror that hierarchy:
MessageBuilder
└── EmailMessageBuilder
The important goal is that EmailMessageBuilder reuses the common message-building methods from MessageBuilder:
.to(...)
.subject(...)
.low_priority()
.normal_priority()
.high_priority()
while adding email-specific methods:
.html(...)
.cc(...)
.bcc(...)
The final usage should work like this:
email = (
EmailMessageBuilder()
.to("ALICE@EXAMPLE.COM")
.subject(" Your invoice ")
.high_priority()
.html(" <p>Thanks for your purchase.</p> ")
.cc("ACCOUNTS@EXAMPLE.COM")
.bcc("AUDIT@EXAMPLE.COM")
.build()
)
The final objects are immutable dataclasses.
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Message:
recipient: str
subject: str
priority: str
@dataclass(frozen=True)
class EmailMessage(Message):
html_body: str
cc: tuple[str, ...] = field(default_factory=tuple)
bcc: tuple[str, ...] = field(default_factory=tuple)
EmailMessage inherits the common message fields from Message, then adds email-specific fields.
MessageBuilder owns the common construction state:
class MessageBuilder:
VALID_PRIORITIES = {"low", "normal", "high"}
def __init__(self):
self._recipient = None
self._subject = None
self._priority = "normal"
It provides common fluent methods:
def to(self, recipient: str) -> Self:
self._recipient = recipient.strip().lower()
return self
def subject(self, subject: str) -> Self:
self._subject = subject.strip()
return self
def low_priority(self) -> Self:
self._priority = "low"
return self
def normal_priority(self) -> Self:
self._priority = "normal"
return self
def high_priority(self) -> Self:
self._priority = "high"
return self
The return type is Self, so when these methods are inherited by EmailMessageBuilder, fluent chaining still returns the subclass builder.
That means this works:
EmailMessageBuilder().to("alice@example.com").html("<p>Hello</p>")
After .to(...), the chain is still an EmailMessageBuilder.
Instead of using a method like _validate_common_fields(), this solution uses a cooperative validation method:
def validate(self) -> None:
...
The base builder validates base fields:
def validate(self) -> None:
if not self._recipient:
raise ValueError("Recipient is required")
if not self._subject:
raise ValueError("Subject is required")
if self._priority not in self.VALID_PRIORITIES:
raise ValueError("Priority must be 'low', 'normal', or 'high'")
Then build() calls validate():
def build(self) -> Message:
self.validate()
return Message(
recipient=self._recipient,
subject=self._subject,
priority=self._priority,
)
This approach scales better if there are more inheritance layers.
Each subclass can do this:
def validate(self) -> None:
super().validate()
# validate fields introduced by this subclass
So validation follows the inheritance chain naturally.
EmailMessageBuilder inherits all common message-building methods from MessageBuilder, then adds email-specific state:
class EmailMessageBuilder(MessageBuilder):
def __init__(self):
super().__init__()
self._html_body = None
self._cc = []
self._bcc = []
It also adds email-specific fluent methods:
def html(self, html_body: str) -> Self:
self._html_body = html_body.strip()
return self
def cc(self, recipient: str) -> Self:
recipient = recipient.strip().lower()
if not recipient:
raise ValueError("CC recipient cannot be blank")
self._cc.append(recipient)
return self
def bcc(self, recipient: str) -> Self:
recipient = recipient.strip().lower()
if not recipient:
raise ValueError("BCC recipient cannot be blank")
self._bcc.append(recipient)
return self
The subclass extends validation by calling super().validate() first:
def validate(self) -> None:
super().validate()
if not self._html_body:
raise ValueError("HTML body is required")
This is the key part of the solution.
MessageBuilder validates common fields:
recipient
subject
priority
EmailMessageBuilder validates email-specific fields:
html_body
If another subclass were added later, it could follow the same pattern.
The subclass build() method calls self.validate() once, then creates the final EmailMessage directly:
def build(self) -> EmailMessage:
self.validate()
return EmailMessage(
recipient=self._recipient,
subject=self._subject,
priority=self._priority,
html_body=self._html_body,
cc=tuple(self._cc),
bcc=tuple(self._bcc),
)
This avoids creating a temporary base Message just to reuse validation.
email = (
EmailMessageBuilder()
.to("ALICE@EXAMPLE.COM")
.subject(" Your invoice ")
.high_priority()
.html(" <p>Thanks for your purchase.</p> ")
.cc("ACCOUNTS@EXAMPLE.COM")
.bcc("AUDIT@EXAMPLE.COM")
.build()
)
The result is:
EmailMessage(
recipient="alice@example.com",
subject="Your invoice",
priority="high",
html_body="<p>Thanks for your purchase.</p>",
cc=("accounts@example.com",),
bcc=("audit@example.com",),
)
This solution demonstrates builder inheritance clearly:
| Part | Responsibility |
|---|---|
MessageBuilder |
Common message fields and validation. |
EmailMessageBuilder |
Email-specific fields and validation. |
Self return type |
Preserves fluent chaining through inherited builder methods. |
validate() |
Lets each inheritance layer validate its own fields. |
super().validate() |
Ensures parent validation still runs. |
The main design idea is:
Each builder validates the fields it owns, then delegates upward with
super().validate().
This avoids awkward method names like _validate_common_fields() and scales better if the hierarchy gets one or two levels deeper.
This approach is good for shallow inheritance.
If the hierarchy becomes very deep, builder inheritance may become awkward:
MessageBuilder
└── EmailMessageBuilder
└── MarketingEmailBuilder
└── PromotionalEmailBuilder
At that point, composition or builder facets may be clearer.
A useful rule:
Builder inheritance is good when the final objects have a real inheritance relationship and the builder hierarchy stays shallow.