Notes and exercises for learning design patterns
In the previous exercise, you split a builder into facets so each part of the object had its own focused builder.
In this exercise, you will practice builder inheritance.
Builder inheritance is useful when the objects being built also use inheritance.
For example:
Message
└── EmailMessage
The child builder should reuse the parent builder’s fluent methods and add child-specific methods.
Do not change these 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 extends Message by adding:
html_body
cc
bcc
Implement two builders:
MessageBuilder
EmailMessageBuilder
EmailMessageBuilder should inherit from MessageBuilder.
The parent builder should provide common message-building methods.
The child builder should reuse those methods and add email-specific methods.
This should work:
message = (
MessageBuilder()
.to("alice@example.com")
.subject("System alert")
.high_priority()
.build()
)
This should also work:
email = (
EmailMessageBuilder()
.to("alice@example.com")
.subject("Your invoice")
.normal_priority()
.html("<p>Thanks for your purchase.</p>")
.cc("accounts@example.com")
.bcc("audit@example.com")
.build()
)
The important part is this:
EmailMessageBuilder().to(...).subject(...).html(...)
The .to(...) and .subject(...) methods are inherited from MessageBuilder, but chaining should still continue with .html(...), which belongs only to EmailMessageBuilder.
Implement:
MessageBuilder
EmailMessageBuilder
You may also add helper methods if useful.
MessageBuilder should collect:
| Field | Default |
|---|---|
recipient |
required |
subject |
required |
priority |
"normal" |
It should support these methods:
.to(recipient)
.subject(subject)
.low_priority()
.normal_priority()
.high_priority()
.build()
All fluent methods should return self.
build() should return a Message.
EmailMessageBuilder should inherit from MessageBuilder.
It should reuse:
.to(...)
.subject(...)
.low_priority()
.normal_priority()
.high_priority()
It should add:
.html(html_body)
.cc(recipient)
.bcc(recipient)
.build()
All fluent methods should return self.
build() should return an EmailMessage.
Use these defaults:
| Field | Default |
|---|---|
priority |
"normal" |
cc |
empty tuple |
bcc |
empty tuple |
The final build() method should validate the completed object.
Rules for MessageBuilder.build():
recipient is required.subject is required.priority must be "low", "normal", or "high".Additional rules for EmailMessageBuilder.build():
html_body is required.cc recipient must be non-empty.bcc recipient must be non-empty.Apply these normalization rules:
recipient should be stripped and lowercased.cc recipients should be stripped and lowercased.bcc recipients should be stripped and lowercased.subject should be stripped.html_body should be stripped.This should fail because recipient is missing:
message = (
MessageBuilder()
.subject("Hello")
.build()
)
This should fail because subject is missing:
message = (
MessageBuilder()
.to("alice@example.com")
.build()
)
This should fail because email body is missing:
email = (
EmailMessageBuilder()
.to("alice@example.com")
.subject("Hello")
.build()
)
This should fail because cc is blank:
email = (
EmailMessageBuilder()
.to("alice@example.com")
.subject("Hello")
.html("<p>Hello</p>")
.cc(" ")
.build()
)
The starter code is also available in exercise3.py.
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Self
@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)
class MessageBuilder:
def __init__(self):
self._recipient = None
self._subject = None
self._priority = "normal"
def to(self, recipient: str) -> Self:
# TODO
return self
def subject(self, subject: str) -> Self:
# TODO
return self
def low_priority(self) -> Self:
# TODO
return self
def normal_priority(self) -> Self:
# TODO
return self
def high_priority(self) -> Self:
# TODO
return self
def build(self) -> Message:
# TODO: validate and return Message(...)
pass
class EmailMessageBuilder(MessageBuilder):
def __init__(self):
super().__init__()
self._html_body = None
self._cc = []
self._bcc = []
def html(self, html_body: str) -> Self:
# TODO
return self
def cc(self, recipient: str) -> Self:
# TODO
return self
def bcc(self, recipient: str) -> Self:
# TODO
return self
def build(self) -> EmailMessage:
# TODO: validate common Message fields
# TODO: validate EmailMessage-specific fields
# TODO: return EmailMessage(...)
pass
In Python, returning self is usually enough for fluent inheritance to work at runtime.
For type hints, use Self:
from typing import Self
Then inherited methods like this:
def to(self, recipient: str) -> Self:
...
return self
will be understood by type checkers as returning the concrete builder type.
So if EmailMessageBuilder inherits .to(...), the chain can continue with email-specific methods:
EmailMessageBuilder().to("alice@example.com").html("<p>Hello</p>")