01What is the clearest sign of an interface that is too wide?
A class implements a method it cannot do, so the method throws "not supported" or does nothing.
02Open-closed: what should a new payment method cost you?
One new class that implements an existing interface. No edits to code that already works.
03Why prefer composition over inheritance?
An object that holds a part can swap that part. A parent class is fixed when you write the child.
A food delivery app takes payments through Razorpay. The checkout code creates the Razorpay client itself, right where it needs it. It is short and it works.
Then two requests arrive in the same week: take international cards through Stripe, and test checkout without charging a real card. Both mean surgery on the checkout class. This lesson shows the one change that makes both requests easy.
The problem
OrderService holds the business rule: charge the customer, then confirm the order. It creates its own RazorpayGateway.
class RazorpayGateway {
boolean charge(String orderId, int amount) {
System.out.println("Razorpay charged " + amount + " for " + orderId);
return true;
}
}
class OrderService {
private final RazorpayGateway gateway = new RazorpayGateway();
String placeOrder(String orderId, int amount) {
boolean paid = gateway.charge(orderId, amount);
return paid ? "confirmed" : "payment failed";
}
}placeOrder("A17", 450) prints Razorpay charged 450 for A17 and returns confirmed. (The real gateway would call Razorpay's servers. Here it prints a line.)
┌────────────────────────────────────────────┐ │ │ │ ┌─────────────────┐ │ │ │ OrderService │ high level │ │ └────────┬────────┘ │ │ │ creates and uses │ │ ▼ │ │ ┌─────────────────┐ │ │ │ RazorpayGateway │ low level │ │ └─────────────────┘ │ │ ──▶ uses │ │ │ └────────────────────────────────────────────┘
- A class says what the app does.
- A class says how one detail gets done.
Here the high-level class depends on the low-level one. A detail now decides when the business rule must change.
The change requests
"Take international cards through Stripe." You must open OrderService, change the field type, and change the new. The class with the business rule changes, though the rule did not.
"Test that a failed payment shows payment failed." You cannot. OrderService always builds a real RazorpayGateway, so every test talks to Razorpay. You cannot make it say no on demand.
Python and TypeScript test tools can patch the class out of the module, for example unittest.mock.patch or jest.mock. That works, but the test has to know how OrderService is built inside. The design still fights you.
The idea
High-level code and low-level code should both depend on an interface. The high-level side owns that interface.
┌───────────────────────────────────────────────────────┐ │ │ │ ┌────────────────┐ │ │ │ OrderService │ high level │ │ └───────┬────────┘ │ │ │ uses │ │ ▼ │ │ ┌────────────────────────────┐ │ │ │ <<interface>> │ │ │ │ PaymentGateway │ abstraction │ │ ├────────────────────────────┤ │ │ │ charge(orderId, amount) │ │ │ └────────────────────────────┘ │ │ △ △ △ │ │ ╎ ╎ ╎ │ │ ┌─────┴────┐ ┌────┴────┐ ┌────┴────┐ │ │ │ Razorpay │ │ Stripe │ │ Fake │ low level │ │ │ Gateway │ │ Gateway │ │ Gateway │ │ │ └──────────┘ └─────────┘ └─────────┘ │ │ ──▶ uses ╌╌▷ implements │ │ │ └───────────────────────────────────────────────────────┘
- PaymentGateway states what checkout needs: charge an amount, say yes or no. It names no vendor.
- OrderService uses only
PaymentGateway. It never writesnew RazorpayGateway(). - RazorpayGateway, StripeGateway, FakeGateway implement it. Each one is a detail.
Why "inversion"? Before, the arrow went from the business rule down to Razorpay. Now Razorpay's arrow points up, at an interface that the business side defined. The detail depends on the rule, not the other way around.
The fix
Step 1: an interface, passed in from outside
interface PaymentGateway {
boolean charge(String orderId, int amount);
}
class RazorpayGateway implements PaymentGateway {
public boolean charge(String orderId, int amount) {
System.out.println("Razorpay charged " + amount + " for " + orderId);
return true;
}
}
class OrderService {
private final PaymentGateway gateway;
OrderService(PaymentGateway gateway) {
this.gateway = gateway;
}
String placeOrder(String orderId, int amount) {
boolean paid = gateway.charge(orderId, amount);
return paid ? "confirmed" : "payment failed";
}
}PaymentGatewayis the interface from the diagram.OrderServicetakes a gateway in its constructor and keeps it in a field.placeOrderdid not change at all.- Handing an object its parts from outside has a name: . It gets its own lesson later in the course.
Step 2: the Stripe request again
Stripe is now one new class, plus one line at app start, the one place that picks the gateway.
class StripeGateway implements PaymentGateway {
public boolean charge(String orderId, int amount) {
System.out.println("Stripe charged " + amount + " for " + orderId);
return true;
}
}
// at app start
OrderService service = new OrderService(new StripeGateway());OrderService does not change. placeOrder("A18", 900) prints Stripe charged 900 for A18 and returns confirmed.
Step 3: the test request again
A test passes in a . It never calls a real server, and you choose its answer.
class FakeGateway implements PaymentGateway {
private final boolean approve;
int calls = 0;
FakeGateway(boolean approve) {
this.approve = approve;
}
public boolean charge(String orderId, int amount) {
calls++;
return approve;
}
}
// in a test
FakeGateway fake = new FakeGateway(false);
OrderService service = new OrderService(fake);
System.out.println(service.placeOrder("A19", 450)); // payment failed
System.out.println(fake.calls); // 1The test checks two things: the order shows payment failed, and the service charged exactly once. It runs in milliseconds, with no network and no card.
How it runs
- App start to StripeGateway:new()
- App start to OrderService:new(gateway)
- Checkout to OrderService:placeOrder(A18)
- OrderService to StripeGateway:charge(900)
- StripeGateway to OrderService:true
- OrderService to Checkout:confirmed
In a test, the same picture holds with FakeGateway in the third column and the test code in the fourth.
When to use it
Signals in the code
- A business class writes
newfor something outside your code: a database, a payment provider, an email or SMS sender, the clock, an HTTP client. - A vendor's name sits in a business class:
MySqlDatabaseinsideOrderService. - You cannot test a rule without a network, a real database or a real card.
- Swapping one vendor means editing classes that hold business rules.
When it is too much
- Do not hide plain values and the standard library behind interfaces.
new ArrayList<>(), aMoneyclass or a date format can stay concrete. - An interface with one implementation and no fake in tests may be noise. Add it when the second implementation, or the first test, needs it.
Dependency inversion or dependency injection?
| Dependency inversion | Dependency injection | |
|---|---|---|
| What it is | a design rule | a coding technique |
| Says | depend on an interface | pass parts in from outside |
| Where it shows | the class diagram | the constructor |
| Works alone? | needs a way to get the parts | can inject concrete classes too |
They work as a team. Inversion decides which way the arrows point. Injection is the usual way to hand over the part.
SOLID on one screen
This lesson closes the SOLID chapter. Here are all five side by side.
| Principle | The rule | The smell | The fix |
|---|---|---|---|
| Single responsibility | one reason to change | one class, many jobs | split by job |
| Open-closed | add code, do not edit | a growing if-else on type | interface plus new class |
| Liskov substitution | a child keeps its parent's promises | overrides that throw or surprise | fix the hierarchy |
| Interface segregation | use every method you depend on | stub methods that throw | small role interfaces |
| Dependency inversion | depend on interfaces | new of a vendor class | interface, passed in |
- 01
Depend on an interface, not on a concrete class. The business rule owns the interface, and the vendors implement it.
- 02
The arrows invert: the detail points up at the interface, instead of the rule pointing down at the detail.
- 03
Pass the implementation in from outside, through the constructor. A vendor switch becomes one new class and one line at app start.
- 04
A fake that implements the same interface tests the rule with no network and no real card.
Read more
- Dependency inversion principle on Wikipedia
- Inversion of control containers and the dependency injection pattern by Martin Fowler, the article that named dependency injection
- Test double by Martin Fowler: fakes, stubs and mocks
- The java.sql package, JDBC's interfaces and
DriverManager
Next chapter: creational design patterns, starting with what design patterns are, and are not.