D · SOLID
Dependency Inversion
High-level policy shouldn't know the low-level details it uses — both should depend on a shared abstraction instead.
Hard-wired to one concrete channel
The high-level service reaches straight into a low-level detail. Swapping the channel — or testing without sending real email — means editing the service itself.
class NotificationService {
constructor() {
this.sender = new EmailSender(); // hard-wired!
}
notify(msg) {
this.sender.sendEmail(msg);
}
}
Both depend on an abstraction, inject it
The service declares what it needs — a MessageSender — and
receives one from outside. It never constructs a concretion itself.
class NotificationService {
constructor(sender) {
this.sender = sender; // MessageSender iface
}
notify(msg) {
this.sender.send(msg);
}
}
// each concretion implements send(msg):
class EmailSender { send(m) {…} }
class SmsSender { send(m) {…} }
class PushSender { send(m) {…} }
Inject a channel — the service never changes
Pick a concrete MessageSender, then watch notify('Hi')
play out as a call/return sequence.
The NotificationService
code is identical in every run — only the injected sender (2nd actor) changes.
The service depends on the MessageSender abstraction, so the same send()
call drives any concretion (DIP).
Depend on abstractions, not concretions
- High-level modules own the interface — low-level details implement it, not the other way round.
- Inject the dependency instead of constructing it inside the class that uses it.
- Unlocks: dependency injection, testing with mocks/fakes, and swapping implementations freely.
- You already use it: this is the engine behind Strategy and Adapter, and the principled fix for a hard-wired Singleton.