- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
Briefly, adhering Dependency Inversion Principle (DIP) makes our code more flexible and adaptable to change.
Lets walk through and example violating DIP.
class User {
const User({required this.name, required this.age});
final String name;
final int age;
}
class HiveStorage {
void save(dynamic item) {
print('Saving to Hive Storage...');
}
}
class UserRepository {
final storage = HiveStorage();
void addUser(User user) {
storage.save(user);
}
}
void main() {
final user = User(name: 'John', age: 30);
final userRepository = UserRepository();
userRepository.addUser(user);
}
Given example shows us UserRepository is dependent on HiveStorage. (Hive is simple and a fast nosql storage solution for Flutter)
As we can see in the diagram, high level module UserRepository has a direct dependency to a low level model which is HiveStorage. Because it knows about implementation details such as what kind of action should be performed to save something to Hive.
Here we have a problem. If we would like to replace Hive with ObjectBox which is another local storage option, then we would have to make a change in UserRepository which is a high level module. However if UserRepository is a high level module and should not have to know about the storage api at all, why are we making these changes in UserRepository? That’s questionable.
On the other hand, same conflict would occur if we would like to add another type of storage instead of a replacement. How can we figure it out?
Let’s go back to the definition of DIP again.
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Suggestion is clear, create an abstraction and make the modules dependent to it.
What if we create an IStorage interface in the middle of UserRepository and HiveStorage?
In this case, UserRepository is only dependent on an abstraction which is the IStorage interface and therefore it does not have to know about what sort of storage API we are using, instead it only depends on the interface and using it’s functions. That’s why for UserRepository, it is not important when HiveStorage is changed with something else such as Objectbox.
And pay attention to the arrows, second arrow is not in the same direction as the first one, it is inverted.
Here is how our code would look like if we adhere DIP.
class User {
const User({required this.name, required this.age});
final String name;
final int age;
}
abstract interface class IStorage {
void save(dynamic item);
}
class HiveStorage implements IStorage {
@override
void save(dynamic item) {
print('Saving to Hive Storage...');
}
}
class ObjectBoxStorage implements IStorage {
@override
void save(dynamic item) {
print('Saving to ObjectBox Storage...');
}
}
class UserRepository {
UserRepository(this.storage);
final IStorage storage;
void addUser(User user) {
storage.save(user);
}
}
void main() {
final user = User(name: 'John', age: 30);
final userRepository = UserRepository(HiveStorage());
userRepository.addUser(user);
}
Using the code below, making a change from hive to objectbox would be only one line of change.
final userRepository = UserRepository(ObjectBoxStorage());
Here is how it looks.
Referefences:
https://en.wikipedia.org/wiki/Dependency_inversion_principle