Two more principles, and the technique that puts them to work: give each class only the methods it calls, point your code at abstractions you own, and pass the real parts in from outside so a test can pass in fakes.
Your delivery app charges a card at checkout, and after 11 pm it adds a 40 rupee late-night fee. You want to test that rule. How do you make it be 11:30 pm in a test, and how do you stop the test from charging a real card?
If the checkout builds its own payment SDK and reads the machine's clock itself, you cannot. The fix is not a clever test. It is the shape of the code, and it has names: the interface segregation principle, the dependency inversion principle and dependency injection. They are the last two letters of SOLID, plus the technique that most often delivers them. The first three letters are in the previous lesson.
You have already seen all of this at work: the parking lot takes its pricing in its constructor, the rate limiter takes a Clock, and food delivery talks to a PaymentGateway with a fake. This lesson says why they were built that way.
A lamp has a plug, not a wire into the wall. Any matching socket powers it, and a test bench can use a battery pack instead.
The first version: a checkout that builds its own SDK and reads the clock#
Here is the checkout most people write first. The vendor's SDK is created inside the constructor, and the late-night rule asks the machine for the time:
# checkout_v1.py: a checkout that builds its own payment SDK and reads the clock itself
from datetime import datetime
class CardSdk: # stands in for a vendor's card SDK: every call is a live charge
live_charges = 0
def charge(self, token: str, rupees: int) -> bool: # rupees is sent to the vendor; the stub only logs the token
CardSdk.live_charges += 1
print(f"[live sdk] charged {token}")
return True
class Checkout:
def __init__(self) -> None:
self.sdk = CardSdk() # built here: every Checkout talks to the live service
def is_late_night(self) -> bool:
hour = datetime.now().hour # the machine's clock, read here
return hour >= 23 or hour < 5
def total(self, subtotal: int) -> int:
return subtotal + (40 if self.is_late_night() else 0) # late-night delivery fee
def pay(self, token: str, subtotal: int) -> str:
ok = self.sdk.charge(token, self.total(subtotal))
return "paid" if ok else "payment failed"
checkout = Checkout()
checkout.pay("tok_asha", 499) # a "test" of pay(): it charges a real card
print("live charges made by that test:", CardSdk.live_charges)
print("total is 499 or 539, depending on the hour it runs:", checkout.total(499) in (499, 539))// CheckoutV1.java: a checkout that builds its own payment SDK and reads the clock itself
import java.time.LocalTime;
class CheckoutV1 {
static final class CardSdk { // stands in for a vendor's card SDK: every call is a live charge
static int liveCharges = 0;
boolean charge(String token, int rupees) { // rupees is sent to the vendor; the stub only logs the token
liveCharges++;
System.out.println("[live sdk] charged " + token);
return true;
}
}
static final class Checkout {
private final CardSdk sdk = new CardSdk(); // built here: every Checkout talks to the live service
boolean isLateNight() {
int hour = LocalTime.now().getHour(); // the machine's clock, read here
return hour >= 23 || hour < 5;
}
int total(int subtotal) {
return subtotal + (isLateNight() ? 40 : 0); // late-night delivery fee
}
String pay(String token, int subtotal) {
return sdk.charge(token, total(subtotal)) ? "paid" : "payment failed";
}
}
public static void main(String[] args) {
Checkout checkout = new Checkout();
checkout.pay("tok_asha", 499); // a "test" of pay(): it charges a real card
System.out.println("live charges made by that test: " + CardSdk.liveCharges);
int total = checkout.total(499);
System.out.println("total is 499 or 539, depending on the hour it runs: " + ((total == 499 || total == 539) ? "True" : "False"));
}
}// checkout_v1.cpp: a checkout that builds its own payment SDK and reads the clock itself
#include <ctime>
#include <iostream>
#include <string>
class CardSdk { // stands in for a vendor's card SDK: every call is a live charge
public:
static int live_charges;
bool charge(const std::string& token, int /*rupees: sent to the vendor; the stub only logs the token*/) {
++live_charges;
std::cout << "[live sdk] charged " << token << "\n";
return true;
}
};
int CardSdk::live_charges = 0;
class Checkout {
CardSdk sdk_; // built here: every Checkout talks to the live service
public:
bool is_late_night() const {
std::time_t now = std::time(nullptr);
int hour = std::localtime(&now)->tm_hour; // the machine's clock, read here
return hour >= 23 || hour < 5;
}
int total(int subtotal) const { return subtotal + (is_late_night() ? 40 : 0); } // late-night delivery fee
std::string pay(const std::string& token, int subtotal) {
return sdk_.charge(token, total(subtotal)) ? "paid" : "payment failed";
}
};
int main() {
Checkout checkout;
checkout.pay("tok_asha", 499); // a "test" of pay(): it charges a real card
std::cout << "live charges made by that test: " << CardSdk::live_charges << "\n";
int total = checkout.total(499);
std::cout << "total is 499 or 539, depending on the hour it runs: " << ((total == 499 || total == 539) ? "True" : "False") << "\n";
}// checkoutV1.js: a checkout that builds its own payment SDK and reads the clock itself
class CardSdk { // stands in for a vendor's card SDK: every call is a live charge
static liveCharges = 0;
charge(token, rupees) { // rupees is sent to the vendor; the stub only logs the token
CardSdk.liveCharges += 1;
console.log(`[live sdk] charged ${token}`);
return true;
}
}
class Checkout {
constructor() {
this.sdk = new CardSdk(); // built here: every Checkout talks to the live service
}
isLateNight() {
const hour = new Date().getHours(); // the machine's clock, read here
return hour >= 23 || hour < 5;
}
total(subtotal) {
return subtotal + (this.isLateNight() ? 40 : 0); // late-night delivery fee
}
pay(token, subtotal) {
return this.sdk.charge(token, this.total(subtotal)) ? "paid" : "payment failed";
}
}
const checkout = new Checkout();
checkout.pay("tok_asha", 499); // a "test" of pay(): it charges a real card
console.log("live charges made by that test:", CardSdk.liveCharges);
const total = checkout.total(499);
console.log("total is 499 or 539, depending on the hour it runs:", total === 499 || total === 539 ? "True" : "False");[live sdk] charged tok_asha
live charges made by that test: 1
total is 499 or 539, depending on the hour it runs: TrueThe test at the bottom meant to check pay() and charged a real card instead: live charges made by that test: 1. The last line is the other problem. The total is 499 or 539 depending on the hour you run it, so the output of this program is not a fact about the code but about the clock.
- A test spends real money.
Checkout()creates a liveCardSdkand cannot be given another one. - A time rule is only tested at the right time. The late-night fee can be checked between 23:00 and 05:00. A test that passes at 3 pm says nothing about it, and one written for 3 pm fails in the night build.
- You cannot ask "what if the card is declined?" The live service decides, and will not decline on demand.
- The vendor is welded in. Changing the vendor means editing
Checkout, the class that holds the business rule. - The signature lies.
Checkout()looks like it needs nothing. It needs a payment service and a clock, and only the body says so.
What makes the late-night rule untestable?
You want a test for "at 23:30 the total is 539". The first checkout is written in Java or C++. What exactly stops you, and what would have to change in the code, not the test?
One strong answer
The checkout reads the clock itself, so the test cannot choose the hour. In Python or JavaScript you could patch the time function at run time, which works but ties the test to one import path and still hides the dependency. In Java you would need a mocking tool that intercepts static calls such as LocalTime.now(), and in C++ a link-time trick. What has to change is the code: the checkout must be given a clock, which the test can set to 23:30, and given its payment service the same way, so a test can hand it a fake instead of a live SDK.
Interface segregation: don't depend on methods you don't call#
Robert C. Martin states the interface segregation principle as: clients should not be forced to depend on methods they do not use. The word that matters is clients: the principle is about each caller's view of a class, not about how many methods a class has. A class may be large. What a given caller sees of it should be only what that caller needs.
The vendor's gateway offers four things: charge a card, refund a payment, pay out to a merchant, and download a settlement report. The first design most teams write is one interface with all four, and hands it to everyone:
# gateway_fat.py: one fat gateway interface, so a checkout test must write four methods to use one
from abc import ABC, abstractmethod
class NotSupported(Exception):
pass
class PaymentGateway(ABC): # one interface for every client of the payment vendor
@abstractmethod
def charge(self, token: str, rupees: int) -> bool: ...
@abstractmethod
def refund(self, payment_ref: str) -> None: ...
@abstractmethod
def payout(self, merchant: str, rupees: int) -> None: ...
@abstractmethod
def settlement_report(self, day: str) -> str: ...
class Checkout: # calls charge() and nothing else
def __init__(self, gateway: PaymentGateway) -> None:
self.gateway = gateway
def pay(self, token: str, rupees: int) -> bool:
return self.gateway.charge(token, rupees)
class FakeGateway(PaymentGateway): # what a checkout test has to write
def __init__(self) -> None:
self.charged = []
def charge(self, token: str, rupees: int) -> bool:
self.charged.append((token, rupees))
return True
def refund(self, payment_ref: str) -> None:
raise NotSupported("refund")
def payout(self, merchant: str, rupees: int) -> None:
raise NotSupported("payout")
def settlement_report(self, day: str) -> str:
raise NotSupported("settlement_report")
fake = FakeGateway()
Checkout(fake).pay("tok_asha", 499)
print("charged", fake.charged[0][0], fake.charged[0][1])
only_to_throw = 0
for call in (lambda: fake.refund("pay_1"), lambda: fake.payout("shop", 100), lambda: fake.settlement_report("mon")):
try:
call()
except NotSupported:
only_to_throw += 1
print("methods this fake can only throw from:", only_to_throw)// GatewayFat.java: one fat gateway interface, so a checkout test must write four methods to use one
import java.util.ArrayList;
import java.util.List;
class GatewayFat {
static final class NotSupported extends RuntimeException {
NotSupported(String what) { super(what); }
}
interface PaymentGateway { // one interface for every client of the payment vendor
boolean charge(String token, int rupees);
void refund(String paymentRef);
void payout(String merchant, int rupees);
String settlementReport(String day);
}
static final class Checkout { // calls charge() and nothing else
private final PaymentGateway gateway;
Checkout(PaymentGateway gateway) { this.gateway = gateway; }
boolean pay(String token, int rupees) { return gateway.charge(token, rupees); }
}
static final class FakeGateway implements PaymentGateway { // what a checkout test has to write
final List<String> charged = new ArrayList<>();
public boolean charge(String token, int rupees) {
charged.add(token + " " + rupees);
return true;
}
public void refund(String paymentRef) { throw new NotSupported("refund"); }
public void payout(String merchant, int rupees) { throw new NotSupported("payout"); }
public String settlementReport(String day) { throw new NotSupported("settlement_report"); }
}
public static void main(String[] args) {
FakeGateway fake = new FakeGateway();
new Checkout(fake).pay("tok_asha", 499);
System.out.println("charged " + fake.charged.get(0));
int onlyToThrow = 0;
List<Runnable> calls = List.of(() -> fake.refund("pay_1"), () -> fake.payout("shop", 100), () -> fake.settlementReport("mon"));
for (Runnable call : calls) {
try {
call.run();
} catch (NotSupported e) {
onlyToThrow++;
}
}
System.out.println("methods this fake can only throw from: " + onlyToThrow);
}
}// gateway_fat.cpp: one fat gateway interface, so a checkout test must write four methods to use one
#include <functional>
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
struct NotSupported : std::runtime_error {
explicit NotSupported(const std::string& what) : std::runtime_error(what) {}
};
class PaymentGateway { // one interface for every client of the payment vendor
public:
virtual ~PaymentGateway() = default;
virtual bool charge(const std::string& token, int rupees) = 0;
virtual void refund(const std::string& payment_ref) = 0;
virtual void payout(const std::string& merchant, int rupees) = 0;
virtual std::string settlement_report(const std::string& day) = 0;
};
class Checkout { // calls charge() and nothing else
PaymentGateway& gateway_;
public:
explicit Checkout(PaymentGateway& gateway) : gateway_(gateway) {}
bool pay(const std::string& token, int rupees) { return gateway_.charge(token, rupees); }
};
class FakeGateway : public PaymentGateway { // what a checkout test has to write
public:
std::vector<std::pair<std::string, int>> charged;
bool charge(const std::string& token, int rupees) override {
charged.emplace_back(token, rupees);
return true;
}
void refund(const std::string&) override { throw NotSupported("refund"); }
void payout(const std::string&, int) override { throw NotSupported("payout"); }
std::string settlement_report(const std::string&) override { throw NotSupported("settlement_report"); }
};
int main() {
FakeGateway fake;
Checkout(fake).pay("tok_asha", 499);
std::cout << "charged " << fake.charged[0].first << " " << fake.charged[0].second << "\n";
int only_to_throw = 0;
std::vector<std::function<void()>> calls = {
[&] { fake.refund("pay_1"); }, [&] { fake.payout("shop", 100); }, [&] { fake.settlement_report("mon"); }};
for (auto& call : calls) {
try {
call();
} catch (const NotSupported&) {
++only_to_throw;
}
}
std::cout << "methods this fake can only throw from: " << only_to_throw << "\n";
}// gatewayFat.js: one fat gateway interface, so a checkout test must write four methods to use one
class NotSupported extends Error {}
class PaymentGateway { // one interface for every client of the payment vendor (JavaScript has no interface keyword: this is the contract)
charge(token, rupees) { throw new NotSupported("charge"); }
refund(paymentRef) { throw new NotSupported("refund"); }
payout(merchant, rupees) { throw new NotSupported("payout"); }
settlementReport(day) { throw new NotSupported("settlementReport"); }
}
class Checkout { // calls charge() and nothing else
constructor(gateway) {
this.gateway = gateway;
}
pay(token, rupees) {
return this.gateway.charge(token, rupees);
}
}
class FakeGateway extends PaymentGateway { // what a checkout test has to write
constructor() {
super();
this.charged = [];
}
charge(token, rupees) {
this.charged.push([token, rupees]);
return true;
}
}
const fake = new FakeGateway();
new Checkout(fake).pay("tok_asha", 499);
console.log("charged", fake.charged[0][0], fake.charged[0][1]);
let onlyToThrow = 0;
for (const call of [() => fake.refund("pay_1"), () => fake.payout("shop", 100), () => fake.settlementReport("mon")]) {
try {
call();
} catch (e) {
if (e instanceof NotSupported) onlyToThrow += 1;
}
}
console.log("methods this fake can only throw from:", onlyToThrow);charged tok_asha 499
methods this fake can only throw from: 3The checkout calls charge and nothing else, but the only gateway type has four methods, so its test fake has three it can only throw from (in JavaScript they come from the base class). That is the first cost of a fat interface. There are two more:
- Changes ripple to clients that never use the changed method. If
settlement_report's signature changes, the interface changes: in Java or C++ the checkout is typically recompiled and retested though it never calls it, and in any language every implementation, fakes included, must be updated. - A client holds more power than it needs. Whoever is handed the gateway can call
payout; a checkout handed a type with nopayoutcannot.
A fake that throws "not supported" is also the broken promise from the previous lesson: fat interfaces and Liskov violations arrive together, because the extra methods are the ones some implementers cannot honour.
The fix is role interfaces, one per client's need. The vendor class can still do everything. Each caller is handed only its role:
%% title: Interface segregation: one role interface per client
classDiagram
class Charger {
<<interface>>
+charge(token, rupees) bool
}
class Refunder {
<<interface>>
+refund(payment_ref)
}
class SettlementReader {
<<interface>>
+settlement_report(day) str
}
class Checkout
class SupportDesk
class FinanceJob
class SdkGateway
Checkout --> Charger : charges with
SupportDesk --> Refunder : refunds with
FinanceJob --> SettlementReader : reads
Charger <|.. SdkGateway
Refunder <|.. SdkGateway
SettlementReader <|.. SdkGatewayEach client points at the one interface its job needs and cannot see the other two, while one class (SdkGateway) implements all three. A test for Checkout now writes one method, and a change to the settlement report touches SettlementReader and FinanceJob only. The program in the next section has Charger and Refunder in code.
Two honest notes on where it stops:
- A role is not one method. If a client always uses
chargeandcapturetogether, they are one interface. Splitting to one method per interface by reflex gives you a pile of tiny types and no benefit. Group by what a client uses together. - Dynamic languages need less of it. In Python and JavaScript a fake needs only the methods the caller calls, so a fat interface costs clarity, not forced stubs. Python's
typing.Protocolwrites the role down; in JavaScript it lives in a comment or in TypeScript.
One gateway class can serve three clients. Give each client an interface with only the methods it calls, so a fake writes one method, not four.
Dependency inversion: point the arrows at abstractions#
The dependency inversion principle, again Robert C. Martin's, has two parts:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
"High-level" means the part that holds the rule you care about: the checkout and its late-night fee. "Low-level" means the mechanism: the vendor SDK, the system clock, the database driver. Left alone, the arrows point downhill, from policy to mechanism, so a change in the mechanism reaches up into the policy.
Inversion means the arrow flips. The checkout does not point at CardSdk. It points at Charger, an interface written in the checkout's own terms (charge(token, rupees) returns a yes or a no), and the SDK side adapts to it. This is where most explanations go wrong: it is not enough to put an interface between them. The interface has to belong to the high-level side. If Charger mirrored the vendor's types (paise, status strings, the vendor's exception classes), the checkout would still depend on the vendor, one step removed.
%% title: Dependency inversion: Checkout points at interfaces it owns
classDiagram
class Checkout {
+total(subtotal) int
+pay(token, subtotal) str
}
class Charger {
<<interface>>
+charge(token, rupees) bool
}
class Clock {
<<interface>>
+hour() int
}
class SdkCharger
class CardSdk
class FakeCharger
class SystemClock
class FixedClock
Checkout --> Charger : charges with
Checkout --> Clock : asks the hour
Charger <|.. SdkCharger
Charger <|.. FakeCharger
Clock <|.. SystemClock
Clock <|.. FixedClock
SdkCharger --> CardSdk : wrapsLook at the arrows that leave SdkCharger: the one that implements Charger points up at the checkout's interface. That is the inversion: at run time the checkout calls down into the SDK, but in the source code the SDK side depends on the checkout's interface. SdkCharger is an adapter (the Adapter pattern has its own lesson later in this part), and it is the only class that knows the vendor counts in paise and answers with a status string.
The checkout points at a Charger it owns, in its own terms. The SDK side adapts to it, so the source code arrow points up at the checkout.
Martin's own caveat matters: the rule is about volatile details, the ones you expect to change or that you need to replace in a test. You do not put an interface in front of a string or a list; you do in front of a vendor, a clock, a database or a network call.
Dependency injection: pass the parts in#
Dependency injection (DI, a name Martin Fowler gave the technique in 2004) is the simplest way to get there: a class receives the objects it depends on from outside, instead of creating or looking them up itself. There are three common places to receive them:
| Way | Looks like | Use it when |
|---|---|---|
| Constructor | Checkout(charger, clock) |
The object needs the part for its whole life. The default choice: the object can never exist half-wired. |
| Method parameter | checkout.total(subtotal, clock) |
Only one call needs it, or it changes per call. |
| Setter | checkout.set_clock(clock) |
Rarely. The object can exist without it, and now sometimes does. Use it only for an optional part. |
Something still has to create the real objects. That place is the composition root: the one spot, usually the start of the program, that names the real classes and wires them together. Here is the whole checkout rebuilt that way, with the role interfaces, the adapter, a fixed clock and a fake charger for tests:
# checkout_di.py: small interfaces owned by the checkout, a real adapter, fakes for tests, one wiring point
from abc import ABC, abstractmethod
from datetime import datetime
class Charger(ABC): # what the checkout needs: nothing about the vendor
@abstractmethod
def charge(self, token: str, rupees: int) -> bool: ...
class Refunder(ABC): # a different client (the support tool) needs this, so it is a different interface
@abstractmethod
def refund(self, payment_ref: str) -> None: ...
class Clock(ABC):
@abstractmethod
def hour(self) -> int: ... # 0 to 23
class Checkout: # depends on two abstractions and builds neither
def __init__(self, charger: Charger, clock: Clock) -> None:
self.charger, self.clock = charger, clock
def total(self, subtotal: int) -> int:
hour = self.clock.hour()
return subtotal + (40 if hour >= 23 or hour < 5 else 0) # late-night delivery fee
def pay(self, token: str, subtotal: int) -> str:
total = self.total(subtotal)
return f"paid {total}" if self.charger.charge(token, total) else "payment failed"
class CardSdk: # the vendor's SDK: paise, status strings, live charges
live_charges = 0
def create_charge(self, token: str, paise: int) -> str:
CardSdk.live_charges += 1
print(f"[live sdk] charged {token}")
return "APPROVED"
class SdkCharger(Charger, Refunder): # the detail adapts to the abstraction, not the other way round
def __init__(self, sdk: CardSdk) -> None:
self.sdk = sdk
def charge(self, token: str, rupees: int) -> bool:
return self.sdk.create_charge(token, rupees * 100) == "APPROVED"
def refund(self, payment_ref: str) -> None:
pass # would call the vendor's refund endpoint
class SystemClock(Clock):
def hour(self) -> int:
return datetime.now().hour
class FixedClock(Clock): # a test double: the hour is whatever the test says
def __init__(self, hour: int) -> None:
self._hour = hour
def hour(self) -> int:
return self._hour
class FakeCharger(Charger): # records what it was asked, answers as told
def __init__(self, approve: bool = True) -> None:
self.approve, self.seen = approve, []
def charge(self, token: str, rupees: int) -> bool:
self.seen.append(f"{token} {rupees}")
return self.approve
fake = FakeCharger()
print("hour 14 ->", Checkout(fake, FixedClock(14)).pay("tok_asha", 499), f"(fake saw: {fake.seen[-1]})")
print("hour 23 ->", Checkout(fake, FixedClock(23)).pay("tok_asha", 499), f"(fake saw: {fake.seen[-1]})")
print("declined card ->", Checkout(FakeCharger(approve=False), FixedClock(14)).pay("tok_asha", 499))
print("live charges made by these tests:", CardSdk.live_charges)
# composition root: the only place that names the real classes
Checkout(SdkCharger(CardSdk()), SystemClock()).pay("tok_asha", 499)// CheckoutDi.java: small interfaces owned by the checkout, a real adapter, fakes for tests, one wiring point
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
class CheckoutDi {
interface Charger { boolean charge(String token, int rupees); } // what the checkout needs: nothing about the vendor
interface Refunder { void refund(String paymentRef); } // a different client (the support tool) needs this, so it is a different interface
interface Clock { int hour(); } // 0 to 23
static final class Checkout { // depends on two abstractions and builds neither
private final Charger charger;
private final Clock clock;
Checkout(Charger charger, Clock clock) {
this.charger = charger;
this.clock = clock;
}
int total(int subtotal) {
int hour = clock.hour();
return subtotal + (hour >= 23 || hour < 5 ? 40 : 0); // late-night delivery fee
}
String pay(String token, int subtotal) {
int total = total(subtotal);
return charger.charge(token, total) ? "paid " + total : "payment failed";
}
}
static final class CardSdk { // the vendor's SDK: paise, status strings, live charges
static int liveCharges = 0;
String createCharge(String token, int paise) {
liveCharges++;
System.out.println("[live sdk] charged " + token);
return "APPROVED";
}
}
static final class SdkCharger implements Charger, Refunder { // the detail adapts to the abstraction, not the other way round
private final CardSdk sdk;
SdkCharger(CardSdk sdk) { this.sdk = sdk; }
public boolean charge(String token, int rupees) { return sdk.createCharge(token, rupees * 100).equals("APPROVED"); }
public void refund(String paymentRef) { } // would call the vendor's refund endpoint
}
static final class SystemClock implements Clock {
public int hour() { return LocalTime.now().getHour(); }
}
static final class FixedClock implements Clock { // a test double: the hour is whatever the test says
private final int hour;
FixedClock(int hour) { this.hour = hour; }
public int hour() { return hour; }
}
static final class FakeCharger implements Charger { // records what it was asked, answers as told
private final boolean approve;
final List<String> seen = new ArrayList<>();
FakeCharger(boolean approve) { this.approve = approve; }
public boolean charge(String token, int rupees) {
seen.add(token + " " + rupees);
return approve;
}
String last() { return seen.get(seen.size() - 1); }
}
public static void main(String[] args) {
FakeCharger fake = new FakeCharger(true);
System.out.println("hour 14 -> " + new Checkout(fake, new FixedClock(14)).pay("tok_asha", 499) + " (fake saw: " + fake.last() + ")");
System.out.println("hour 23 -> " + new Checkout(fake, new FixedClock(23)).pay("tok_asha", 499) + " (fake saw: " + fake.last() + ")");
System.out.println("declined card -> " + new Checkout(new FakeCharger(false), new FixedClock(14)).pay("tok_asha", 499));
System.out.println("live charges made by these tests: " + CardSdk.liveCharges);
// composition root: the only place that names the real classes
new Checkout(new SdkCharger(new CardSdk()), new SystemClock()).pay("tok_asha", 499);
}
}// checkout_di.cpp: small interfaces owned by the checkout, a real adapter, fakes for tests, one wiring point
#include <ctime>
#include <iostream>
#include <string>
#include <vector>
class Charger { // what the checkout needs: nothing about the vendor
public:
virtual ~Charger() = default;
virtual bool charge(const std::string& token, int rupees) = 0;
};
class Refunder { // a different client (the support tool) needs this, so it is a different interface
public:
virtual ~Refunder() = default;
virtual void refund(const std::string& payment_ref) = 0;
};
class Clock {
public:
virtual ~Clock() = default;
virtual int hour() const = 0; // 0 to 23
};
class Checkout { // depends on two abstractions and builds neither
Charger& charger_;
const Clock& clock_;
public:
Checkout(Charger& charger, const Clock& clock) : charger_(charger), clock_(clock) {}
int total(int subtotal) const {
int hour = clock_.hour();
return subtotal + (hour >= 23 || hour < 5 ? 40 : 0); // late-night delivery fee
}
std::string pay(const std::string& token, int subtotal) {
int t = total(subtotal);
return charger_.charge(token, t) ? "paid " + std::to_string(t) : "payment failed";
}
};
class CardSdk { // the vendor's SDK: paise, status strings, live charges
public:
static int live_charges;
std::string create_charge(const std::string& token, int /*paise*/) {
++live_charges;
std::cout << "[live sdk] charged " << token << "\n";
return "APPROVED";
}
};
int CardSdk::live_charges = 0;
class SdkCharger : public Charger, public Refunder { // the detail adapts to the abstraction, not the other way round
CardSdk& sdk_;
public:
explicit SdkCharger(CardSdk& sdk) : sdk_(sdk) {}
bool charge(const std::string& token, int rupees) override { return sdk_.create_charge(token, rupees * 100) == "APPROVED"; }
void refund(const std::string&) override {} // would call the vendor's refund endpoint
};
class SystemClock : public Clock {
public:
int hour() const override {
std::time_t now = std::time(nullptr);
return std::localtime(&now)->tm_hour;
}
};
class FixedClock : public Clock { // a test double: the hour is whatever the test says
int hour_;
public:
explicit FixedClock(int hour) : hour_(hour) {}
int hour() const override { return hour_; }
};
class FakeCharger : public Charger { // records what it was asked, answers as told
bool approve_;
public:
std::vector<std::string> seen;
explicit FakeCharger(bool approve = true) : approve_(approve) {}
bool charge(const std::string& token, int rupees) override {
seen.push_back(token + " " + std::to_string(rupees));
return approve_;
}
};
int main() {
FakeCharger fake;
FixedClock fourteen(14), twentyThree(23);
std::cout << "hour 14 -> " << Checkout(fake, fourteen).pay("tok_asha", 499) << " (fake saw: " << fake.seen.back() << ")\n";
std::cout << "hour 23 -> " << Checkout(fake, twentyThree).pay("tok_asha", 499) << " (fake saw: " << fake.seen.back() << ")\n";
FakeCharger declining(false);
std::cout << "declined card -> " << Checkout(declining, fourteen).pay("tok_asha", 499) << "\n";
std::cout << "live charges made by these tests: " << CardSdk::live_charges << "\n";
// composition root: the only place that names the real classes
CardSdk sdk;
SdkCharger real(sdk);
SystemClock clock;
Checkout(real, clock).pay("tok_asha", 499);
}// checkoutDi.js: small interfaces owned by the checkout, a real adapter, fakes for tests, one wiring point
// JavaScript has no interface keyword: Charger is "anything with charge(token, rupees)", Clock is "anything with hour()".
class Checkout { // depends on two abstractions and builds neither
constructor(charger, clock) {
this.charger = charger;
this.clock = clock;
}
total(subtotal) {
const hour = this.clock.hour();
return subtotal + (hour >= 23 || hour < 5 ? 40 : 0); // late-night delivery fee
}
pay(token, subtotal) {
const total = this.total(subtotal);
return this.charger.charge(token, total) ? `paid ${total}` : "payment failed";
}
}
class CardSdk { // the vendor's SDK: paise, status strings, live charges
static liveCharges = 0;
createCharge(token, paise) {
CardSdk.liveCharges += 1;
console.log(`[live sdk] charged ${token}`);
return "APPROVED";
}
}
class SdkCharger { // the detail adapts to the abstraction, not the other way round
constructor(sdk) {
this.sdk = sdk;
}
charge(token, rupees) {
return this.sdk.createCharge(token, rupees * 100) === "APPROVED";
}
refund(paymentRef) {} // would call the vendor's refund endpoint
}
class SystemClock {
hour() {
return new Date().getHours();
}
}
class FixedClock { // a test double: the hour is whatever the test says
constructor(hour) {
this._hour = hour;
}
hour() {
return this._hour;
}
}
class FakeCharger { // records what it was asked, answers as told
constructor(approve = true) {
this.approve = approve;
this.seen = [];
}
charge(token, rupees) {
this.seen.push(`${token} ${rupees}`);
return this.approve;
}
}
const fake = new FakeCharger();
console.log("hour 14 ->", new Checkout(fake, new FixedClock(14)).pay("tok_asha", 499), `(fake saw: ${fake.seen.at(-1)})`);
console.log("hour 23 ->", new Checkout(fake, new FixedClock(23)).pay("tok_asha", 499), `(fake saw: ${fake.seen.at(-1)})`);
console.log("declined card ->", new Checkout(new FakeCharger(false), new FixedClock(14)).pay("tok_asha", 499));
console.log("live charges made by these tests:", CardSdk.liveCharges);
// composition root: the only place that names the real classes
new Checkout(new SdkCharger(new CardSdk()), new SystemClock()).pay("tok_asha", 499);hour 14 -> paid 499 (fake saw: tok_asha 499)
hour 23 -> paid 539 (fake saw: tok_asha 539)
declined card -> payment failed
live charges made by these tests: 0
[live sdk] charged tok_ashaThe numbers, so you can check them: at hour 14 there is no fee and the total is 499. At hour 23 the fee applies, 499 plus 40 is 539, and the fake saw that exact amount. The declined card is a case the live service could never produce on demand. No test touched the vendor: live charges made by these tests: 0. The last line is the composition root, the one place where the real SDK and the real clock are named; its result is not printed, because it depends on the hour it runs.
What each language brings:
- Python can write the interface as an abstract base class, as here, or as a
typing.Protocol, which any object with the right methods satisfies without inheriting. No framework is needed. - Java has
java.time.Clockin the standard library, an injected clock already (Clock.fixed(...)for tests). Frameworks such as Spring can wire constructors for you. - C++ has no standard clock to inject, so you write the interface. The real choice is ownership: a reference parameter, as here, means the caller keeps the object alive;
std::unique_ptrhands it over;std::shared_ptrshares it. - JavaScript needs no interface: anything with a
chargemethod will do, and a plain function works when the part has one operation. Nothing checks the contract until you run it, which TypeScript fixes at compile time.
Hand the checkout a clock instead of letting it read one. The test sets the hour to 23 and gets 539, whatever time it really is.
What to inject, and what to leave alone#
Inject what is slow, outside your process, different on every run, or that you will want to vary. Leave the rest as ordinary calls.
| Inject it | Because | Seen in |
|---|---|---|
| The clock | A time rule is only testable if the test sets the time; the system clock can also jump | Rate limiter: Clock, MonotonicClock, FakeClock |
| Randomness | A seeded generator makes a game of chance repeatable | Snake and ladder: StandardDice takes a seeded random.Random, and ScriptedDice plays fixed rolls |
| Another service | A test must not call it, and must be able to make it fail | ATM: Bank, with FlakyBank; food delivery: PaymentGateway, with a fake |
| A rule that varies | New behaviour should be new code | Parking lot: ParkingLot(floors, assigner, pricing) |
| Leave it alone | Because |
|---|---|
len, max, Math.floor, string and list methods |
Pure, fast, stable and never replaced in a test. |
A small value object such as Money or Line |
It has no outside effects; just build one. |
| A pure function of its arguments | There is nothing to fake. Call it. |
| A part that is cheap and deterministic, such as an in-memory map | Using the real one is a better test than a fake of it. |
Test doubles come in kinds, and the names help in an interview. A stub returns canned answers; a spy records the calls it gets; a fake is a small working version (an in-memory bank); a mock is told which calls to expect and fails if they do not happen. FakeCharger above is a stub and a spy at once.
Is it DIP?
A teammate changes Checkout to take the SDK as a constructor parameter: Checkout(CardSdk sdk). Is that dependency injection? Is it dependency inversion?
One strong answer
It is injection, and it is not inversion. The checkout is given its SDK, so a test can pass in a different CardSdk object, but the checkout still names the vendor's concrete class and still speaks the vendor's language (paise, status strings). Changing vendor still edits the checkout. Inversion needs the checkout to point at Charger, an interface in its own terms, with SdkCharger adapting the vendor to it.
DI and its look-alikes#
| Idea | What it is | How to tell it apart |
|---|---|---|
| Dependency inversion (DIP) | A design rule: depend on abstractions you own | It is about the direction of source-code arrows |
| Dependency injection (DI) | A technique: parts are passed in | It is about how a part arrives |
| Inversion of control (IoC) | The broad idea that something outside your code drives it: a framework calls your handlers, not the other way round ("don't call us, we'll call you") | DI is one form of it, applied to how parts arrive. It is not DIP: IoC is about who calls whom at run time, DIP about which way the source-code arrows point |
| IoC container (Spring, for one) | A tool that builds the objects and does the injection for you | It saves wiring code in a large program. It is optional, and a program with a handful of classes needs none |
| Service locator | A lookup the class calls: Registry.get("charger") |
The class still fetches its own part. It can depend on an abstraction, but the dependency is hidden from the signature, a test must fill (and later reset) the shared registry, and a missing part fails mid-call, not at construction |
| Strategy | An interchangeable behaviour passed in | It is injection, of a rule that varies, with a business reason for the variety. The same mechanism, a different motive |
| A factory | Code that builds an object on request | It creates parts. DI passes parts that already exist. They combine: the composition root is often a factory |
When not to use it#
- An interface in front of every class (DIP). A pricing rule that nobody will replace and no test needs to fake does not need a
PricingRulePort. Add the abstraction at the places that are slow, external or volatile. A fake in a test is a genuine second implementation; a class with one implementation and no test seam is guesswork. - One interface per method (ISP). Group by what one client uses together. A dozen single-method interfaces that every client needs all of is a fat interface again, in pieces.
- A constructor with nine parameters (DI). It is telling you the class has too many responsibilities, which is the first lesson's problem. Split the class; do not hide the parameters in a container.
- A container for a small program. It swaps a few visible lines of wiring for reflection a reader cannot step through. Wire by hand until that is a real burden.
- Mocking the world. A test that mocks every collaborator only checks that the code calls what it calls. Prefer a real, cheap collaborator or a fake with behaviour.
An interface in front of every class is guesswork. Add one where a test or a vendor needs a seam: slow, external or varying parts.
Where it shows up in the case studies#
| Case study | What is injected | How |
|---|---|---|
| Parking lot | Pricing and spot assignment | ParkingLot(floors, assigner, pricing): tests pass a fixed strategy and a fixed time, and time arrives as a parameter (at), never read inside the class |
| Rate limiter | A Clock |
TokenBucket(capacity, refill_per_sec, clock); MonotonicClock in production, and a FakeClock you advance in tests |
| Snake and ladder | The dice | A Dice interface; StandardDice takes a seeded generator, so a seed replays a game, and ScriptedDice plays fixed rolls |
| ATM | The bank | A Bank interface with an InMemoryBank, and a FlakyBank test double that can lose one reply, with no if testing: in the real code |
| Food delivery | The payment gateway | A PaymentGateway interface with two methods (charge, refund), and a FakeGateway that records calls; the service holds the gateway as a field it is given |
None of these rounds reaches for a global: the parking lot lists Singleton as avoided and creates one instance at startup to pass around, which is this lesson's composition root. The Singleton lesson takes that further.
In the interview#
Say the principle with its reason, in one breath: "The checkout depends on a Charger interface and a Clock interface that it owns, and gets them through its constructor, so in a test I pass a fake charger and a fixed clock, and only the composition root names the real SDK."
Then expect the follow-ups:
- "What is the difference between dependency inversion and dependency injection?" Inversion is the design rule: both sides depend on an abstraction owned by the high-level side, so the source-code arrow flips. Injection is how the part arrives: through the constructor, a parameter or a setter. You can inject a concrete class (DI, no DIP).
- "Why constructor injection, not a service locator?" The object cannot exist half-wired, and its dependencies are visible in its signature, so a test just passes fakes. A locator hides them inside the methods and makes every test set up shared registry state. Setter injection leaves a window where the part is missing.
- "How do you test time-dependent code?" Inject a clock: a real one in production and a fixed one in the test. Do not sleep, and do not read the system clock inside the class.
- "Do you need a DI framework?" No. In an interview-sized design, wire by hand and say a container pays off only in a large application.
- "Why not let the vendor define the interface?" The checkout then depends on the vendor's vocabulary, so a vendor change reaches the business rule. Define it by what the checkout needs and adapt the vendor to it.
Common mistakes#
- "Dependency injection is the same as a DI framework." It is passing parts in; a container is one tool for doing it.
- Injecting a concrete class and calling it dependency inversion. The checkout still names the vendor.
- An interface that copies the vendor's API, so nothing is inverted. Write the interface in your own terms.
- "Interface segregation means small interfaces." It means interfaces shaped by each client's use; size follows from that.
- A service locator presented as DI. The class still fetches its own parts.
- Reading the system clock, or creating the random generator, inside the class that has the rule you want to test.
ISP, DIP and DI
Q1/4 A checkout test must write a fake gateway with four methods, though the checkout calls only charge. Which principle is being broken, and what is the fix?
Answer & explanation
Answer: B
Clients should not depend on methods they do not use. A role interface per client gives the checkout a one-method fake. Splitting the checkout or subclassing the real gateway does not change what it depends on.
Q2/4 A checkout takes the vendor's CardSdk object as a constructor parameter. Which statement is correct?
Answer & explanation
Answer: C
Passing the object in is injection. Inversion needs the checkout to depend on an abstraction it owns, with the SDK adapted to it. A test can still replace the object, but changing vendor still edits the checkout.
Q3/4 You need a test for "orders after 11 pm pay a 40 rupee fee". What is the right design?
Answer & explanation
Answer: C
The checkout must be given the time, not read it. A fixed clock in the test makes the rule deterministic and fast. A global that tests overwrite is shared state that leaks between tests.
Q4/4 Which of these is a sensible thing NOT to inject?
Answer & explanation
Answer: D
Inject what is slow, external, nondeterministic or volatile. A pure, stable function has nothing to fake, so an interface in front of it adds files and no value.
this lesson in shorts · 3
play them all →One idea from this lesson in about a minute each, made for your phone.
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.


