I'll Be Backend
Principles · Python

Dependency Inversion: let the policy own the interface

Keep your business rules untouched when you swap how messages are sent.

14 steps4 chapters~6 min at 1.25×#solid#dependency inversion#interfaces#decoupling
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the Python video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–4

Refactor

Steps 5–10

Payoff

Steps 11–12

Recap

Steps 13–14

The code: before and after

Before

@dataclass(frozen=True)
class Customer:
    name: str
    email: str
    phone: str


@dataclass(frozen=True)
class Order:
    id: int
    customer: Customer
    total: int


class SmtpMailer:
    def __init__(self, host: str) -> None:
        self.host = host

    def send_mail(self, to: str, subject: str, body: str) -> None:
        print(f"mail to {to}: {subject}: {body}")


class ReceiptService:
    _mailer: SmtpMailer

    def __init__(self) -> None:
        self._mailer = SmtpMailer("smtp.shop.test")

    def send_receipt(self, order: Order) -> None:
        text = f"Order {order.id} paid: {order.total} EUR"
        self._mailer.send_mail(order.customer.email, "Your receipt", text)


def main() -> None:
    ana = Customer("Ana", "ana@shop.test", "0700 111 222")
    order = Order(42, ana, 30)
    ReceiptService().send_receipt(order)


if __name__ == "__main__":
    main()

After

@dataclass(frozen=True)
class Customer:
    name: str
    email: str
    phone: str


@dataclass(frozen=True)
class Order:
    id: int
    customer: Customer
    total: int


class SmtpMailer:
    def __init__(self, host: str) -> None:
        self.host = host

    def send_mail(self, to: str, subject: str, body: str) -> None:
        print(f"mail to {to}: {subject}: {body}")


class SmsGateway:
    def __init__(self, api_key: str) -> None:
        self.api_key = api_key

    def send_text(self, phone: str, text: str) -> None:
        print(f"text to {phone}: {text}")


class ReceiptChannel(Protocol):
    def send(self, customer: Customer, text: str) -> None: ...


class ReceiptService:
    _channel: ReceiptChannel

    def __init__(self, channel: ReceiptChannel) -> None:
        self._channel = channel

    def send_receipt(self, order: Order) -> None:
        text = f"Order {order.id} paid: {order.total} EUR"
        self._channel.send(order.customer, text)


class EmailReceipts(ReceiptChannel):
    def __init__(self, mailer: SmtpMailer) -> None:
        self._mailer = mailer

    def send(self, customer: Customer, text: str) -> None:
        self._mailer.send_mail(customer.email, "Your receipt", text)


class SmsReceipts(ReceiptChannel):
    def __init__(self, gateway: SmsGateway) -> None:
        self._gateway = gateway

    def send(self, customer: Customer, text: str) -> None:
        self._gateway.send_text(customer.phone, text)


def main() -> None:
    ana = Customer("Ana", "ana@shop.test", "0700 111 222")
    order = Order(42, ana, 30)
    gateway = SmsGateway("demo-key")
    ReceiptService(SmsReceipts(gateway)).send_receipt(order)


if __name__ == "__main__":
    main()

Transcript, step by step

Problem

Step 1: A shop that mails receipts Before

Ana has an email and a phone, and SMTP Mailer sends to an address with a subject and a body. Receipt Service creates that mailer itself in its constructor, builds the receipt text, and mails it to the customer. Receipts were always mailed, so calling the mailer straight from the receipt code was the shortest path.

Step 2: Receipts go by text Change request

Customers read texts faster than mail, so receipts now go to the customer's phone as a text message. The quick way adds SMS Gateway, built with an API key, and its send text method takes a phone and a text. Then Receipt Service changes its attribute type, builds the gateway in its constructor, and calls send text with the phone. Mypy is happy with it, and main now prints the receipt as a text to Ana's phone.

Step 3: The rule depends on a detail Code smell

Receipt Service holds the business rule, what a receipt says, and that code is called the policy. SMS Gateway does one technical job the rule doesn't care about, so it's called a detail. Yet the policy depends on that detail, because it names the gateway's type here, creates it here, and calls its method here. The receipt text line didn't change, but the policy was edited in three places for a change in delivery.

Step 4: Dependency Inversion Principle

The fix is a rule where policy and detail both depend on one interface, and it's called the Dependency Inversion Principle. In plain words, the policy declares the interface it needs, in its own words, and each detail implements it. Right now the dependency points the other way, from the policy down to the detail.

Refactor

Step 5: Refactor first Back it out

We take the gateway back out, so receipts go by mail again. Then we refactor in small steps, and text messages come back once the policy owns its interface.

Step 6: Delivery in its own method Extract Method

So the delivery code needs one place of its own, and the mailer call moves into a private method, deliver. Send receipt now builds the text and hands it to deliver, so the output is the same mail line.

Step 7: Email receipts move out Extract Class

But deliver still lives inside the policy, so it moves into a new class, Email Receipts, as a method named send. Receipt Service now creates an Email Receipts and calls send, so it no longer calls the mailer's method.

Step 8: The policy names its need Extract Interface

We declare a protocol, Python's form of an interface, Receipt Channel, with one method, send, taking a customer and a text. Email Receipts lists Receipt Channel as its base class, so the arrow now points from the mail side up to the interface. That base class also makes mypy check Email Receipts against the interface right where the class is declared. But Receipt Service still holds an Email Receipts, so nothing calls through the interface yet.

Step 9: Who owns the interface Extract Interface

You might put a Mailer interface over SMTP Mailer, with send mail's address, subject and body. But a text has no subject, so the policy would still speak mail, and texts would edit it again. Receipt Channel's shape matters, because it says what the policy needs, a customer and a text, and nothing about hosts or subjects. An interface declared by the code that calls it, in that code's words, is what Martin calls interface ownership. But Receipt Service still names Email Receipts here, so it still depends on the mail detail.

Step 10: Pass the channel in Change Function Declaration

If Receipt Service still built Email Receipts itself, it would still name a mail type, and texts would edit it again. Receipt Service's constructor now takes a Receipt Channel and stores it, and passing it in like this is called constructor injection. The attribute type becomes Receipt Channel too, so Receipt Service names no mail type at all. Main builds the mailer, wraps it in Email Receipts, passes that in, and the same mail line prints. Before, the policy depended on the mailer, and now the detail depends on an interface the policy owns. That change in direction is the inversion the principle is named for.

Payoff

Step 11: Text messages, take two Payoff

Text messages come back now, so how many lines of Receipt Service do you think change? SMS Gateway returns as it was, and SMS Receipts subclasses Receipt Channel and sends the text to the customer's phone. Main builds the gateway with its API key and passes an SMS Receipts in, so the receipt goes to Ana's phone.

Step 12: The policy did not change Payoff

None, because Receipt Service and Receipt Channel didn't change to add text messages. The three places from before now go through Receipt Channel: the attribute here, the constructor here, and the call here. Changing delivery meant adding the text classes and editing main, because the policy owns the interface and the details plug into it. So the policy declares the interface, and each detail implements it. But you pay with one more type to read, and main now has to put the pieces together itself.

Recap

Step 13: Where we started, where we ended Before / after

On the left, Receipt Service created the SMTP Mailer itself and called send mail directly. So every change in delivery meant editing the policy. On the right, Receipt Service gets a Receipt Channel through its constructor, an interface it declares in its own words. SMS Receipts plugs into that interface and keeps the gateway call to itself.

Step 14: When to reach for it Takeaways

Reach for it when a policy calls a detail that changes on its own schedule, like mail or payments. Skip it when the class you call is stable with no second version in sight, like the built-in list. Spot it by a business class that creates its own mail or database client, or an interface that copies a vendor's method. And the logging module already works this way, because it declares Handler and each destination implements emit.