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
type Customer = {
name: string; email: string; phone: string;
};
type Order = {
id: number; customer: Customer; total: number;
};
class SmtpMailer {
constructor(readonly host: string) {}
sendMail(to: string, subject: string, body: string) {
return `mail to ${to}: ${subject}: ${body}`;
}
}
class ReceiptService {
private readonly mailer: SmtpMailer;
constructor() {
this.mailer = new SmtpMailer("smtp.shop.test");
}
sendReceipt(order: Order): string {
const text =
`Order ${order.id} paid: ${order.total} EUR`;
return this.mailer.sendMail(
order.customer.email, "Your receipt", text);
}
}
function shop(): string {
const ana: Customer = {
name: "Ana",
email: "ana@shop.test",
phone: "0700 111 222",
};
const order: Order = { id: 42, customer: ana, total: 30 };
return new ReceiptService().sendReceipt(order);
}After
type Customer = {
name: string; email: string; phone: string;
};
type Order = {
id: number; customer: Customer; total: number;
};
class SmtpMailer {
constructor(readonly host: string) {}
sendMail(to: string, subject: string, body: string) {
return `mail to ${to}: ${subject}: ${body}`;
}
}
class SmsGateway {
constructor(readonly apiKey: string) {}
sendText(phone: string, text: string) {
return `text to ${phone}: ${text}`;
}
}
class EmailReceipts implements ReceiptChannel {
constructor(private readonly mailer: SmtpMailer) {}
send(customer: Customer, text: string) {
return this.mailer.sendMail(customer.email, "Your receipt", text);
}
}
class SmsReceipts implements ReceiptChannel {
constructor(private readonly gateway: SmsGateway) {}
send(customer: Customer, text: string) {
return this.gateway.sendText(customer.phone, text);
}
}
interface ReceiptChannel {
send(customer: Customer, text: string): string;
}
class ReceiptService {
constructor(private readonly channel: ReceiptChannel) {}
sendReceipt(order: Order): string {
const text =
`Order ${order.id} paid: ${order.total} EUR`;
return this.channel.send(order.customer, text);
}
}
function shop(): string {
const ana: Customer = {
name: "Ana",
email: "ana@shop.test",
phone: "0700 111 222",
};
const order: Order = { id: 42, customer: ana, total: 30 };
const gateway = new SmsGateway("demo-key");
return new ReceiptService(new SmsReceipts(gateway)).sendReceipt(order);
}Transcript, step by step
Problem
Step 1: A shop that mails receipts Before
SMTP Mailer is built with a host, and its send mail method takes an address, a subject and a body. Receipt Service creates that mailer itself, builds the receipt text, and mails it, even though Ana also has a phone number here. 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 text messages 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 message. Then Receipt Service changes its field type, builds the gateway in its constructor, and calls send text with the phone. It compiles, and shop now returns the receipt as a text message 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. And the policy can't run at all without building a real gateway with its API key.
Step 4: Dependency Inversion Principle
So the policy shouldn't depend on the detail, and both should depend on an interface the policy declares. That rule is the Dependency Inversion Principle. 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 mail call has to come out of send receipt before anything can replace it. First, the mailer call moves into a private method called deliver, which takes a customer and the receipt text. Send receipt now builds the receipt text and hands it to deliver, so shop returns the same mail line.
Step 7: Email receipts move out Extract Class
But deliver still sits inside the policy, so a second channel would still mean editing Receipt Service. Deliver moves into a new class, Email Receipts, which holds the SMTP Mailer, and the method is renamed to send. Email Receipts wraps the detail and fits it to what the policy asks, and a wrapper like that is an adapter. 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 an interface, Receipt Channel, whose one method sends a receipt text to a customer. In TypeScript, any class with a matching send method already fits Receipt Channel by its shape. Writing implements says so out loud, and the compiler checks Email Receipts right where it's declared. Nothing calls through the interface yet, so the output is the same mail line.
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 message has no subject, so the policy would still speak mail, and text messages would edit it again. Receipt Channel's shape matters, because it says what the policy needs, a customer and a receipt text, 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 text messages would edit it again. So its constructor now takes a Receipt Channel, and passing it in like this is called constructor injection. Private readonly on that parameter makes it the field too, so Receipt Service names no mail type at all. Shop builds the mailer, wraps it in Email Receipts, passes that in, and gets the same mail line back. Before, the policy depended on the mailer, and now the adapter 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
So the text message request from the start can come back in. How many lines of Receipt Service will change this time? SMS Gateway returns as it was, and SMS Receipts implements Receipt Channel by sending the receipt text to the customer's phone. Shop 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. Of the three places from before, the field and the call name only Receipt Channel, and the creation moved to shop. Changing delivery meant adding the text message classes and editing shop, because the policy owns the interface they plug into. The policy declares the interface and each adapter implements it, so the dependency now points up to the policy. But you pay with one more type to read, and shop 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 Map. Spot it by a business class that creates its own mail or database client, or an interface that copies a vendor's method. And for of loops already work this way, because the language declares the iterable protocol and any class can implement it.