Original Book Cover:

Guideline 9: Pay Attention to the Ownership of Abstractions
Reading Notes:

1. Explanation
In software development, change is the only constant. One of the key elements in handling change is the introduction of abstractions, but simply adding base classes or templates is not enough; attention must also be paid to the ownership of these added abstractions.
2. Dependency Inversion Principle (DIP)
Robert Martin proposed: The most flexible systems are those whose source code dependencies point only to abstractions rather than concrete implementations.
This is the famous Dependency Inversion Principle (the fifth of the SOLID principles). The Dependency Inversion Principle focuses on inverting the unreasonable dependencies of abstractions on concrete implementations to dependencies of concrete implementations on abstractions.
3. Initial Problem Scenario
3.1 For example, consider the initial design of an ATM system:
// Base class for transactions
class Transaction {
// ...
};
class Deposit : public Transaction {
// Directly depends on a concrete UI class
void execute() {
int amount = ui.requestDepositAmount();
// ... Handle deposit logic
}
ConcreteUI ui;
};
class Withdrawal : public Transaction {
void execute() {
int amount = ui.requestWithdrawalAmount();
// ... Handle withdrawal logic
}
ConcreteUI ui;
};
// Concrete UI class
class ConcreteUI {
requestDepositAmount();
requestWithdrawalAmount();
};

Problem: All transaction classes directly depend on the concrete UI class, leading to:
- • Modifications to the UI class are required when adding new transaction types
- • Indirect coupling between transaction classes through the UI class
- • High-level (transaction) depends on low-level (UI)
3.2 Solution: Introduce Abstract Interfaces
// High-level owns the abstract interface
class DepositUI {
public:
virtual ~DepositUI() = default;
virtual int requestDepositAmount() = 0;
};
// Transaction class now depends on the abstraction
class Deposit : public Transaction {
private:
DepositUI& ui;
public:
Deposit(DepositUI& ui_ref) : ui(ui_ref) {}
void execute() override {
int amount = ui.requestDepositAmount();
// ... Handle deposit logic
}
};
// Concrete UI class inherits and reverses dependency on DepositUI
class ConcreteUI : public DepositUI{
virtual requestDepositAmount() override{...};
virtual requestWithdrawalAmount override(//do nothing);
};

- • Note: The ownership of the newly introduced abstract interface should belong to the high level. As shown in the figure: by adding a new abstract class DepositUI, the original dependency of Deposit on UI is inverted to a dependency of UI on DepositUI, thus reversing the dependency to the upper abstract layer.
4. Plugin Architecture Example
4.1 Incorrect Plugin Design
// Low-level owns the abstraction - incorrect approach
// ---- <thirdparty/Plugin.h> ----
class Plugin { /*...*/ };
// ---- <thirdparty/VimModePlugin.h> ----
#include <thirdparty/Plugin.h>
class VimModePlugin : public Plugin { /*...*/ };
// ---- <yourcode/Editor.h> ----
#include <thirdparty/Plugin.h> // Incorrect dependency direction!
class Editor {
// Editor depends on the third-party defined plugin interface
};

4.2 Correct Plugin Design
// High-level owns the abstraction - correct approach
// ---- <yourcode/Plugin.h> ----
class Plugin {
public:
virtual ~Plugin() = default;
virtual void initialize() = 0;
virtual void execute() = 0;
virtual void cleanup() = 0;
};
// ---- <yourcode/Editor.h> ----
#include "Plugin.h"
class Editor {
private:
std::vector<std::unique_ptr<Plugin>> plugins;
public:
void addPlugin(std::unique_ptr<Plugin> plugin) {
plugins.push_back(std::move(plugin));
}
};
// ---- <thirdparty/VimModePlugin.h> ----
#include <yourcode/Plugin.h> // Correct dependency direction
class VimModePlugin : public Plugin {
// Implement plugin interface
};

As shown in the figure: The ownership of the abstract class Plugin should belong to the high level, allowing the application of the Dependency Inversion Principle to reverse the dependency direction of the low-level concrete implementation class VimModePlugin to depend on the high level.
5. Implementing Dependency Inversion through Templates
// STL algorithm implements dependency inversion through concepts
template<typename InputIt, typename OutputIt, typename UnaryPredicate>
OutputIt copy_if(InputIt first, InputIt last, OutputIt d_first,
UnaryPredicate pred)
{
for (; first != last; ++first) {
if (pred(*first)) {
*d_first++ = *first;
}
}
return d_first;
}
// Usage example
#include <vector>
#include <algorithm>
int main() {
std::vector<int> source = {1, 2, 3, 4, 5};
std::vector<int> destination;
// Low-level (container and predicate) depends on high-level (algorithm) defined requirements
std::copy_if(source.begin(), source.end(),
std::back_inserter(destination),
[](int x) { return x % 2 == 0; });
return 0;
}

6. Implementing Dependency Inversion through Overload Sets
// Widget template definition
template<typename T>
struct Widget {
T value;
};
// Provide swap overload for Widget
template<typename T>
void swap(Widget<T>& lhs, Widget<T>& rhs) {
using std::swap;
swap(lhs.value, rhs.value); // Depends on T's swap overload
}
// Usage example
#include <string>
#include <cassert>
int main() {
Widget<std::string> w1{"Hello"};
Widget<std::string> w2{"World"};
swap(w1, w2); // Use custom swap
assert(w1.value == "World");
assert(w2.value == "Hello");
return 0;
}

7. Conclusion
7.1 Ownership Determines Dependency Direction
- • Abstractions should be owned by high-level modules
- • Low-level modules should depend on abstractions defined by high-level modules
- • This ensures that the dependency arrows point in the correct direction
7.2 Two Key Points of the Dependency Inversion Principle
1. High-level modules should not depend on low-level modules; both should depend on abstractions.
2. Abstractions should not depend on details; details should depend on abstractions.
7.3 Implementation Methods
- • Inheritance hierarchy: Base classes defined by high-level
- • Templates and concepts: Requirements defined by algorithms
- • Overload sets: Semantic expectations defined by high-level
7.4 Relationship with the Single Responsibility Principle
- • DIP focuses on the dependency direction at the architectural level
- • SRP focuses on the single responsibility within modules
- • Both ensure the maintainability and extensibility of the system
7.5 Practical Recommendations
1. Identify architectural levels: Clarify which are stable high-level modules and which are volatile low-level modules.
2. Define abstract interfaces: Define abstract interfaces in high-level modules.
3. Control dependency direction: Ensure low-level modules depend on high-level modules.
4. Code organization: Organize header files and include relationships according to dependencies.
- • Note: Dependency inversion is not just about introducing abstractions; more importantly, it is about the ownership of abstractions. Only by placing abstractions at the high level of the architecture can true dependency inversion be achieved.
8. Selected Quotes from the Original Text
- • The most flexible systems are those in which source code dependencies refer only to abstractions, not to concretions.
- • High-level modules should not depend on low-level modules. Both should depend on abstractions.
- • Abstractions should not depend on details. Details should depend on abstractions.
- • Make sure abstractions are owned by the high level, not by the low level.