design·lab

D · SOLID

Dependency Inversion

High-level policy shouldn't know the low-level details it uses — both should depend on a shared abstraction instead.

✗ The problem

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);
  }
}
High-level policy depends on a concrete low-level class — can't swap channels, can't test without sending real email.
✓ The refactor

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) {…} }
NotificationService
notify()
MessageSender
interface · send()
Email
SMS
Push
✓ See it live

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).

✓ Takeaway

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.