cconnected.
Understand the pattern. Find your own way.Find your starting point
Your learning feed/Idea 158 of 218
Ideas this builds on
Low-level design158 / 218

Agree on a small promise

Picture

A reminder needs to send one message. It should not need to know network credentials just to decide the wording. A sender with a small send(message) promise can be passed in from outside.

Predict

How can a test verify the reminder message without contacting an email service?

Walk through the small example

Supply a recording sender that appends messages to a list. The reminder keeps the same workflow and can be tested without external side effects.

Mental model and complete Python solution

The Protocol describes the structural interface expected by a type checker. Reminder accepts a collaborator through its constructor: dependency injection. RecordingSender satisfies the shape without inheriting from Protocol. The contract says send returns normally when accepted or raises on failure; Reminder does not swallow that failure. Type annotations are not runtime validation and cannot prove delivery semantics. A real email adapter would implement this boundary separately.

What the function promises: Reminder.send_for(name) builds and submits one message to its supplied sender. RecordingSender only records in memory and never performs real delivery.

from typing import Protocol

class Sender(Protocol):
    def send(self, message: str) -> None: ...

class RecordingSender:
    def __init__(self):
        self.messages = []

    def send(self, message: str) -> None:
        self.messages.append(message)

class Reminder:
    def __init__(self, sender: Sender):
        self.sender = sender

    def send_for(self, name: str) -> None:
        self.sender.send(f"{name}, your saved idea is ready to revisit.")

sender = RecordingSender()
Reminder(sender).send_for("Ana")
assert sender.messages == ["Ana, your saved idea is ready to revisit."]
assert RecordingSender().messages == []

Work and storage: Message creation is linear in message length; delivery cost belongs to the sender implementation. The value of injection is controllability and separation, not an asymptotic improvement.

When to recognize it: The service needs a send operation, not knowledge of how delivery happens.

Check before applying it: Python type hints do not validate behavior at runtime. The replacement must honor the same error and side-effect contract.

Change one rule

A replacement sender silently drops all messages and returns normally. Does matching the method signature make it a valid replacement?

See what changes

No. It violates the acceptance/delivery promise agreed for the boundary. Substitution is about observable behavior as well as method names and types.

Connect

functions: Input and output contracts still matter between objects.

Make failure and retries part of the design: Inject a controlled collaborator to test failure and retry behavior.