Dependency Inversion: let the policy own the interface
Keep your business rules untouched when you swap how messages are sent.
Watch the narrated video
Every step
Problem
Steps 1–4Refactor
Steps 5–10Payoff
Steps 11–12Recap
Steps 13–14The code: before and after
Before
final readonly class Customer
{
public function __construct(
public string $name,
public string $email,
public string $phone,
) {}
}
final readonly class Order
{
public function __construct(
public int $id,
public Customer $customer,
public int $total,
) {}
}
final readonly class SmtpMailer
{
public function __construct(
public string $host,
) {}
public function sendMail(string $to, string $subject, string $body): void
{
echo "mail to {$to}: {$subject}: {$body}\n";
}
}
final class ReceiptService
{
private readonly SmtpMailer $mailer;
public function __construct()
{
$this->mailer = new SmtpMailer('smtp.shop.test');
}
public function sendReceipt(Order $order): void
{
$text = "Order {$order->id} paid: {$order->total} EUR";
$this->mailer->sendMail(
$order->customer->email,
'Your receipt',
$text,
);
}
}
$ana = new Customer('Ana', 'ana@shop.test', '0700 111 222');
$order = new Order(42, $ana, 30);
(new ReceiptService())->sendReceipt($order);After
final readonly class Customer
{
public function __construct(
public string $name,
public string $email,
public string $phone,
) {}
}
final readonly class Order
{
public function __construct(
public int $id,
public Customer $customer,
public int $total,
) {}
}
final readonly class SmtpMailer
{
public function __construct(
public string $host,
) {}
public function sendMail(string $to, string $subject, string $body): void
{
echo "mail to {$to}: {$subject}: {$body}\n";
}
}
final readonly class SmsGateway
{
public function __construct(
public string $apiKey,
) {}
public function sendText(string $phone, string $text): void
{
echo "text to {$phone}: {$text}\n";
}
}
final readonly class EmailReceipts implements ReceiptChannel
{
public function __construct(
private SmtpMailer $mailer,
) {}
public function send(Customer $customer, string $text): void
{
$this->mailer->sendMail($customer->email, 'Your receipt', $text);
}
}
final readonly class SmsReceipts implements ReceiptChannel
{
public function __construct(
private SmsGateway $gateway,
) {}
public function send(Customer $customer, string $text): void
{
$this->gateway->sendText($customer->phone, $text);
}
}
interface ReceiptChannel
{
public function send(Customer $customer, string $text): void;
}
final class ReceiptService
{
public function __construct(
private readonly ReceiptChannel $channel,
) {}
public function sendReceipt(Order $order): void
{
$text = "Order {$order->id} paid: {$order->total} EUR";
$this->channel->send($order->customer, $text);
}
}
$ana = new Customer('Ana', 'ana@shop.test', '0700 111 222');
$order = new Order(42, $ana, 30);
$gateway = new SmsGateway('demo-key');
(new ReceiptService(new SmsReceipts($gateway)))->sendReceipt($order);Transcript, step by step
Problem
Step 1: A shop that mails receipts Before
Ana's order 42 goes to Receipt Service, which mails the receipt through SMTP Mailer's send mail method. 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 property type, builds the gateway in its constructor, and calls send text with the phone. The script 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 when it goes out, 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
Right now the dependency points down, from the policy to the gateway. So the policy should declare an interface in its own words, and a small class around the gateway should implement it.
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 delivery gets its own home first, and the mailer call moves into a private method called 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 out into a new class, Email Receipts. Email Receipts gets the mailer through its constructor, and deliver becomes its send method. 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
Right next to Receipt Service we declare the Receipt Channel interface, with one method, send, that takes a customer and a text. Email Receipts implements it, but nothing calls through the interface yet.
Step 9: Who owns the interface Extract Interface
You might put a mailer interface over SMTP Mailer instead, 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. The Receipt Channel interface 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 Robert 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. Its constructor now takes the channel as a parameter, and passing it in like this is called constructor injection. That parameter is the property now, so its type is the Receipt Channel interface and no mail type is left. The script 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
How many lines of Receipt Service do you think have to change for text messages? SMS Gateway returns as it was, and SMS Receipts implements the Receipt Channel interface by sending the text to the customer's phone. The script 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
Zero, because Receipt Service and the Receipt Channel interface didn't change to add text messages. Of the three places, the creation is gone, and the property here and the call here name only the Receipt Channel interface. Changing delivery meant adding SMS Receipts and editing the script, because the policy owns the interface and the details plug into it. The policy declares the interface and each detail implements it, and that's called the Dependency Inversion Principle. But you pay with one more type to read, and the script 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 the channel through its constructor, an interface it declares in its own words. Its send call names only that interface, so the mailer and the gateway never appear in it.
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 standard date classes. Spot it by a business class that creates its own mail or database client, or an interface that copies a vendor's method. And PHP sessions already work this way, because the session module declares a handler interface and your storage class implements it.