Dependency Inversion Principle
Your code should depend on abstractions, not concrete implementations. Instead of:
public class EmailSender
{
public void Send(string message) { ... }
}
public class NotificationService
{
private readonly EmailSender _emailSender = new();
public void Notify(string message) => _emailSender.Send(message);
}
… have:
public interface IMessageSender
{
void Send(string message);
}
public class EmailMessageSender : IMessageSender
{
public void Send(string message) { ... }
}
public class NotificationService(IMessageSender messageSender)
{
public void Notify(string message) => messageSender.Send(message);
}
… so that swapping out an EmailMessageSender for an SmsMessageSender is
easy. Even more commonly, unit testing NotificationService with a
MockMessageSender that doesn’t send real messages.
Notes from Chromium
Phrased differently, inversion of control allows users of a framework/library
(clients) to customize the behavior of the framework. The control flow is
inverted because the library (e.g., NotificationService) calls back into the
client’s code (e.g., the specific IMessageSender implementation) so that
control flows back from a low-level class to a high-level class. .
Dependency injection (via the constructor) is one technique of achieving dependency inversion .
The low-level class can execute a callback, e.g.,
class StringKVStore {
public:
// ...
using KeyPredicate = base::RepeatingCallback<bool(const string&)>;
};
void StringKVStore::GetKeysMatching(const KeyPredicate& predicate) {
set<string> keys;
for (const auto& key : internal_keys()) {
if (predicate.Run(key)) keys.insert(key);
}
return keys;
}
… provided by the higher-level class, e.g.,
bool IsKeyInteresting(const string& key) { ... }
void YourFunction() {
StringKVStore::KeyPredicate = base::BindRepeating(&IsKeyInteresting);
for (const auto& key : kv_store_.GetKeysMatching(predicate)) { ... }
}
While callbacks are heavyweights in performance, memory usage, and nested setup code, they have several advantages: no separate interface needed; client methods don’t need specific names; client methods can be bound with any needed state; avoid multiple inheritance; can be passed around by themselves.
Observers are useful when more than one client cares to passively listen to events happening without modifying the state of the framework object being observed, e.g.,
class StringKVStore::Observer {
public:
virtual void OnKeyChanged(StringKVStore* store,
const string& key,
const string& from_value,
const string& to_value) {}
};
class StringKVStore {
public:
// ...
void AddObserver(Observer* observer);
void RemoveObserver(Observer* observer);
// ...
void Put(const string& key, const string& value) {
optional<string> maybe_old_value = Get(key);
// ...
if (maybe_old_value.has_value()) {
for (const auto& observer : observers_)
observer.OnKeyChanged(this, key, maybe_old_value.value(), value);
}
}
};
class HelloKeyWatcher : public StringKVStore::Observer {
public:
void OnKeyChanged(StringKVStore* store,
const string& key,
const string& from_value,
const string& to_value) override {
if (key == "hello") ++hello_changes_;
}
};
Delegates are responsible for implementing part of the framework that is deliberately missing, e.g.,
class StringKVStore::Delegate {
public:
// Use case: Provide logic that happens synchronously with what's happening in
// the framework.
virtual bool ShouldPersistKey(StringKVStore* store, const string& key);
// Allow clients to inject their own subclasses of framework objects that need
// to be constructed by the framework.
virtual unique_ptr<StringKVStoreBackend> CreateBackend(StringKVStore* store);
};
References
- Design Principles | Hello Interview Low Level Design. www.hellointerview.com . Accessed Sep 13, 2026.
- Chromium Docs - docs/patterns/inversion-of-control.md. chromium.googlesource.com . Accessed Sep 13, 2026.
See deeper dive on observer patterns .