The full picture
interface Notifier { void send(String to, String msg); }
final class EmailNotifier implements Notifier { … }
final class OrderService {
private final OrderRepo repo; // depends on ABSTRACTIONS
private final Notifier notifier;
OrderService(OrderRepo repo, Notifier notifier) { // constructor injection
this.repo = repo; this.notifier = notifier;
}
}
// The composition root — the ONLY place that knows concretes:
public static void main(String[] a) {
var service = new OrderService(new JdbcOrderRepo(ds), new EmailNotifier(smtp));
}
// Tests: new OrderService(new InMemoryRepo(), (to, msg) -> {}) — no framework, no mocks needed- Service locator vs DI: a locator hides dependencies (you must read the body to know them) and couples everything to the locator — DI keeps the signature honest.
- What frameworks add on top of the pattern: lifecycle management, scopes, AOP interception, configuration binding, conditional wiring — value at scale, not magic.
- Coupling has flavors — worst to best: content/common (shared mutable globals) → control → stamp → data. DI plus immutable message types pushes you toward data coupling.
- A quick cohesion test: can you name the class without 'And' or 'Manager/Util'? Do its methods use most of its fields (LCOM intuition)?