Dependency Inversion Principle
The Dependency Inversion Principle (DIP) is the “D” in the SOLID principles. It states that high-level modules should not depend on low-level modules – both should depend on an abstraction – and that abstractions should not depend on details. The “inversion” is that the low-level detail ends up depending on an interface owned by the policy, rather than the policy depending on the detail.
Why it matters
A high-level module encodes policy – what the system does. A low-level module is a detail – how a particular step is carried out. When the policy reaches down and constructs a concrete detail, it welds the two together: the interesting logic now cannot exist without that exact database, that exact mail server, that exact clock. You cannot swap the detail for another, and you cannot exercise the policy in a test without dragging the real detail along with it.
Pointing both sides at an abstraction breaks the weld. The policy depends on an interface it defines in its own terms; the detail implements that interface. Now the detail can be replaced – a different provider, or a test double in a test – without the policy noticing. DIP is the principle; dependency injection is the usual mechanism that realizes it, supplying the concrete implementation from outside rather than letting the policy build one.
Example
Consider a password-reset policy that needs to send a message. Constructing the SMTP mailer inside the policy ties the two together permanently.
class SmtpMailer {
public:
auto send(const std::string& to, const std::string& body) -> void { /* ... */ }
};
// High-level policy welded to the low-level detail it constructs.
class PasswordReset {
public:
auto request(const std::string& user) -> void {
m_mailer.send(user, "Reset your password: ...");
}
private:
SmtpMailer m_mailer;
};
package auth
type SMTPMailer struct{}
func (SMTPMailer) Send(to, body string) { /* ... */ }
// PasswordReset is welded to the low-level detail it constructs.
type PasswordReset struct {
mailer SMTPMailer
}
func NewPasswordReset() *PasswordReset {
return &PasswordReset{mailer: SMTPMailer{}}
}
func (p *PasswordReset) Request(user string) {
p.mailer.Send(user, "Reset your password: ...")
}
class SmtpMailer:
def send(self, to: str, body: str) -> None: ...
# High-level policy welded to the low-level detail it constructs.
class PasswordReset:
def __init__(self) -> None:
self._mailer = SmtpMailer()
def request(self, user: str) -> None:
self._mailer.send(user, "Reset your password: ...")
pub struct SmtpMailer;
impl SmtpMailer {
pub fn send(&self, to: &str, body: &str) { /* ... */ }
}
/// High-level policy welded to the low-level detail it constructs.
pub struct PasswordReset {
mailer: SmtpMailer,
}
impl PasswordReset {
pub fn new() -> Self {
Self { mailer: SmtpMailer }
}
pub fn request(&self, user: &str) {
self.mailer.send(user, "Reset your password: ...");
}
}
If we represent this graphically, we’ll immediately notice that the arrow points the wrong way:
classDiagram
class PasswordReset {
-mailer SmtpMailer
+request(user)
}
class SmtpMailer {
+send(to, body)
}
PasswordReset --> SmtpMailer : constructs and callsThe policy depends on the implementation detail, so switching to SMS, batching
sends, or testing the reset flow without a live mail server all mean editing
PasswordReset:
If we break this up with an abstraction based around the behavior, we would
land with a Notifier abstraction – and this inverts that dependency. The
policy now only depends on Notifier, which is implemented by SmtpMailer:
classDiagram
class PasswordReset {
+request(user)
}
class Notifier {
<<interface>>
+notify(recipient, message)
}
class SmtpMailer {
+notify(recipient, message)
}
PasswordReset --> Notifier : depends on
Notifier <|.. SmtpMailer: implementsWith this inversion, the caller is now responsible for supplying the concrete notifier from outside:
class Notifier {
public:
virtual ~Notifier() = default;
virtual auto notify(const std::string& recipient, const std::string& message) -> void = 0;
};
class SmtpMailer final : public Notifier {
public:
auto notify(const std::string& recipient, const std::string& message) -> void override { /* ... */ }
};
class PasswordReset {
public:
explicit PasswordReset(Notifier& notifier) : m_notifier(¬ifier) {}
auto request(const std::string& user) -> void {
m_notifier->notify(user, "Reset your password: ...");
}
private:
Notifier* m_notifier;
};
package auth
type Notifier interface {
Notify(recipient, message string)
}
type SMTPMailer struct{}
func (SMTPMailer) Notify(recipient, message string) { /* ... */ }
type PasswordReset struct {
notifier Notifier
}
func NewPasswordReset(notifier Notifier) *PasswordReset {
return &PasswordReset{notifier: notifier}
}
func (p *PasswordReset) Request(user string) {
p.notifier.Notify(user, "Reset your password: ...")
}
from typing import Protocol
class Notifier(Protocol):
def notify(self, recipient: str, message: str) -> None: ...
class SmtpMailer:
def notify(self, recipient: str, message: str) -> None: ...
class PasswordReset:
def __init__(self, notifier: Notifier) -> None:
self._notifier = notifier
def request(self, user: str) -> None:
self._notifier.notify(user, "Reset your password: ...")
pub trait Notifier {
fn notify(&self, recipient: &str, message: &str);
}
pub struct SmtpMailer;
impl Notifier for SmtpMailer {
fn notify(&self, recipient: &str, message: &str) { /* ... */ }
}
pub struct PasswordReset<N: Notifier> {
notifier: N,
}
impl<N: Notifier> PasswordReset<N> {
pub fn new(notifier: N) -> Self {
Self { notifier }
}
pub fn request(&self, user: &str) {
self.notifier.notify(user, "Reset your password: ...");
}
}
This inversion now allows:
Better testability: We can easily create a test double to simulate the real object in tests, instead of relying on the SMTP implementation concretely, and
Better scalability: By working with an abstraction, we can now easily create new
Notifierinstances, such as an SMS notifier, phone call notifier, smoke signal, etc.
The rest of SOLID comes along with it. Notifier has a single method, satisfying
interface segregation;
any notifier that honors the contract is
substitutable, so an
SMS notifier or a test double drops in without editing PasswordReset, which is
open/closed. Policy and
transport are now separate units, each with
one responsibility.
In practice you reach DIP through
dependency injection – the
caller constructs the notifier and passes it in.
References
- The Dependency Inversion Principle – Martin’s original article on inverting the direction of source-code dependencies through abstractions.
- Dependency inversion principle – overview, including who should own the abstraction and how it differs from dependency injection.
- Inversion of Control Containers and the Dependency Injection pattern – how injection supplies the concrete detail that DIP tells you to depend on abstractly.