Programming to an interface: widen concrete parameter types
Let one receipt method send through any channel that can deliver it.
Watch the narrated video
Every step
Problem
Steps 1–3Refactor
Steps 4–6Payoff
Steps 7–8Recap
Steps 9–10The code: before and after
Before
type Rental = { days: number; price: number };
const outbox: string[] = [];
class SmsGateway {
private credits = 100;
constructor(private readonly phone: string) {}
send(text: string): void {
this.credits--;
outbox.push(`SMS ${this.phone}: ${text}`);
}
topUp(amount: number): void {
this.credits += amount;
}
}
class Receipts {
static send(rental: Rental, sms: SmsGateway): void {
const text =
`${rental.days} days, ${rental.price} dollars`;
sms.send(text);
}
}
function desk(): string {
const trip: Rental = { days: 3, price: 215 };
Receipts.send(trip, new SmsGateway("555-0101"));
return outbox.join("\n");
}After
type Rental = { days: number; price: number };
const outbox: string[] = [];
interface MessageSender {
send(text: string): void;
}
class SmsGateway implements MessageSender {
private credits = 100;
constructor(private readonly phone: string) {}
send(text: string): void {
this.credits--;
outbox.push(`SMS ${this.phone}: ${text}`);
}
topUp(amount: number): void {
this.credits += amount;
}
}
class EmailGateway implements MessageSender {
constructor(private readonly address: string) {}
send(text: string): void {
outbox.push(`Mail ${this.address}: ${text}`);
}
}
class Receipts {
static send(rental: Rental, sender: MessageSender): void {
const text =
`${rental.days} days, ${rental.price} dollars`;
sender.send(text);
}
}
function desk(): string {
const trip: Rental = { days: 3, price: 215 };
Receipts.send(trip, new SmsGateway("555-0101"));
Receipts.send(trip, new EmailGateway("fleet@acme.com"));
return outbox.join("\n");
}Transcript, step by step
Problem
Step 1: Receipts by text Before
The car rental desk from the last lesson builds its prices from parts, and now it sends each customer a receipt. The SMS Gateway texts one phone and counts credits, and it also has a top up method that receipts never call. Each text lands in the outbox array, which stands in for the phone network. Receipts builds the receipt line from the rental and hands it to the gateway. The desk function sends the receipt for a three day trip, and the outbox it returns holds three days, two hundred fifteen dollars. Text was the only channel, so naming the class it used was the shortest path.
Step 2: Receipts by email Code smell
Business customers now want their receipts by email. The quick way adds an Email Gateway that writes to an address, and a second method that takes it. A TypeScript class allows one body per method name, so that method needs a new name, send email. The desk sends the trip's receipt both ways, and the outbox now holds a mail line under the text line.
Step 3: One method per class Code smell
The receipt method's parameter type is a concrete class, so it accepts that one class only, though it calls just one of its methods, send. TypeScript compares classes by shape, and an Email Gateway has no top up and no private phone, so it doesn't fit that parameter. You can see it in the text parameter here, the email parameter here, and the copied receipt line here. The receipt wording now lives in two methods, so changing dollars to USD takes two edits that can drift apart. And a push channel next would be a third copy of the method.
Refactor
Step 4: Refactor first Back it out
So let's take the email change out again. We'll refactor first, so receipts ask for something any channel can fill, and then email will be one new class.
Step 5: Name what Receipts uses Extract Interface
Receipts calls one method on the gateway, send, and nothing else. So we extract an interface, Message Sender, with that one method, and the SMS Gateway implements it. Top up stays out of it, because receipts never top up, so the interface holds only what Receipts calls. The gateway's send already matches the interface, so its body doesn't change.
Step 6: Widen the parameter Change Signature
Programming to an interface means a method asks for the interface it uses, not one class that has it. So we widen the parameter, which means we change it to a type that more classes fit, and here that type is Message Sender. The body now calls send on the sender, the one method the interface declares. The desk function doesn't change and still passes an SMS Gateway, because an SMS Gateway is a Message Sender.
Payoff
Step 7: Email, as one class Add email
Now email comes back, as an Email Gateway that implements Message Sender. The desk function adds one line, and the outbox gets the mail line under the text line, as before. Receipts, Message Sender and the SMS Gateway didn't change, and there's no send email method.
Step 8: One method, any sender Programming to an interface
The two concrete parameters are now one Message Sender parameter, and the receipt line exists once, here. A push channel would be one more class that implements Message Sender. The Gang of Four put it as program to an interface, not an implementation. It fits here because Receipts calls only send, so when it asks for an interface with just that method, any class that can send fits the parameter.
Recap
Step 9: Where we started, where we ended Before / after
On the left, Receipts asked for an SMS Gateway, though it only ever called send. On the right, it asks for a Message Sender, the interface with that one method. Email Gateway implements it, so email took one new class and no copied method.
Step 10: When to reach for it Takeaways
Reach for it when a method uses a small part of a class, and a second class could do that part. Skip it when only one type will ever fill the parameter, like Rental here, because an interface for a plain data type adds a file and nothing else. You'll spot it by a parameter typed with a concrete class when the method calls only one of its methods, and by copied methods that differ only in that type. Array from takes any Iterable, so a Set, a Map or your own generator fits.