Notes and exercises for learning design patterns
The notification system has matured. EmailNotifier has grown a richer interface beyond
just send:
class EmailNotifier(Notifier):
def send(self, recipient: str, message: str) -> None:
print(f"EMAIL to {recipient}: {message}")
def set_from_address(self, address: str) -> None:
self._from = address
print(f"From address set to {address}")
def set_reply_to(self, address: str) -> None:
self._reply_to = address
print(f"Reply-to set to {address}")
def get_sent_count(self) -> int:
return self._sent_count
def flush_queue(self) -> None:
print("Queue flushed")
The team uses these methods throughout the codebase:
notifier = EmailNotifier()
notifier.set_from_address("alerts@company.com")
notifier.set_reply_to("noreply@company.com")
notifier.send("alice@example.com", "hello")
print(notifier.get_sent_count())
notifier.flush_queue()
Now you wrap EmailNotifier with LoggingDecorator from Exercise 1:
notifier = LoggingDecorator(EmailNotifier())
notifier.set_from_address("alerts@company.com") # AttributeError!
The decorator only exposes send. Everything else is gone.
Run the provided test_type_erosion function and observe the AttributeError.
Read the error carefully. Make sure you understand why it happens before moving on.
A colleague suggests the quick fix: add forwarding methods to LoggingDecorator.
class LoggingDecorator(Notifier):
...
def set_from_address(self, address: str) -> None:
self._wrapped.set_from_address(address)
def set_reply_to(self, address: str) -> None:
self._wrapped.set_reply_to(address)
# ... and so on
Implement this version. Then answer:
EmailNotifier gets a new method set_bcc next week?Rewrite NotifierDecorator (the shared base from Exercise 2) to use __getattr__ forwarding.
After your fix, this should work without any changes to LoggingDecorator:
notifier = LoggingDecorator(EmailNotifier())
notifier.set_from_address("alerts@company.com") # forwards to EmailNotifier
notifier.set_reply_to("noreply@company.com") # forwards to EmailNotifier
notifier.send("alice@example.com", "hello") # intercepted by LoggingDecorator
notifier.flush_queue() # forwards to EmailNotifier
Verify that __getattr__ forwarding works through a full decorator stack, not just one layer.
notifier = LoggingDecorator(
RateLimitDecorator(
PrefixDecorator(EmailNotifier(), prefix="[ALERT] "),
limit=10,
)
)
notifier.set_from_address("alerts@company.com")
notifier.send("alice@example.com", "disk at 95%")
notifier.get_sent_count()
Make sure all three calls work correctly.
Add a type hint to a function that only accepts EmailNotifier:
def configure_notifier(notifier: EmailNotifier) -> None:
notifier.set_from_address("alerts@company.com")
notifier.set_reply_to("noreply@company.com")
Now call it with a decorated notifier:
configure_notifier(LoggingDecorator(EmailNotifier()))
Does it work at runtime? What does a type checker say? What is the trade-off?
See exercise3.py.
__getattr__ (fallback) and __getattribute__ (always fires).__getattr__ in the base replaces N forwarding methods across all decorators.