Dependency Inversion Principle, Dependency Injection, and Service Locator. Are they the same?
Originally published on Medium
So, let’s educate ourselves and discover what is what.
Let’s assume we make a blogging app. Something similar to Medium.
We have several classes :
StoryView
StoryController (or StoryViewModel if you prefer)
StoryServiceFirebase
When the user presses the clap 👏 button StoryView calls storyController.clap().
If you are a member, please continue, otherwise, read the full story here.
story_controller.dart
class StoryController extends GetxConroller {
StoryServiceFirebase service = StoryServiceFirebase(); //problem is here
void clap (times) {
service.clap(times);
}
}
I have omitted some details to make it simple.
Now, we see the problem: StoryController and StoryServiceFirebase are tightly coupled.
The reason is that StoryConroller creates its dependency — StoryServiceFirebase by itself.
Then, if we need to change StoryServiceFirebase we should change StoryConroller as well.
This is a violation of the Open-Closed Principle—“O” in SOLID—which states that the class should be closed for modifications.
Theoretical solution: Dependency Inversion Principle.
DIP — the “D” in SOLID tells us to code against abstractions but not implementations. This way we can get dependency from outside instead of creating them ourselves. Hence, the term “dependency inversion”.
Practical solutions:
IoC container.
Service Locator design pattern.
Dependency Injection design pattern.
IoC (Inversion of Control) is a paraphrase of DIP with an accent on the fact that dependency is created by the container. IoC containers are very popular in Java. We don’t have them (as packages) in Dart/Flutter. They are based on reflection and can introduce performance issues.
A good (?) example of IoC in Flutter is a build(BuildContext context) method. Instead of letting us create an BuildContext
object ourselves, Flutter injects it into every build
method. Here Flutter itself plays as an IoC container.
Service Locator is usually implemented like this:
class ServiceLocator {
static final ServiceLocator _instance = ServiceLocator._();
ServiceLocator._();
factory ServiceLocator() => _instance;
Map<Type, Object> map = {};
void put<Type>(Object value) {
map[Type] = value;
}
Object? find<Type>() {
return map[Type];
}
}
We should not implement Service Locator by ourselves since there is a super popular package — GetIt.
How can it be used to decouple StoryController and StoryServiceFirebase?
First, we need to create the interface StoryService:
abstract class StoryService {
void clap(times);
}
class StoryServiceFirebase implements StoryService {
...
Then, we instantiate StoryServiceFirebase:
GetIt.instance.registerSingleton<StoryService>(StoryServiceFirebase());
Now, we can use it in our StoryController:
class StoryController extends GetxConroller {
void clap (times) {
GetIt.instance<StoryService>().clap(times);
}
}
We have accomplished our mission — StoryController doesn’t know about StoryService implementation anymore.
Why is Service Locator considered anti-pattern?
Two problems:
Dependency became hidden.
When we createstoryController = StoryController();
we cannot see that StoryController depends on StoryService. This is not a big problem since the dependency graph in the Flutter application is very simple. View depends on Controller and Controller depends on Service.We introduce a new dependency — Service Locator itself.
The solution is simple — make Service Locator a global singletone (another “anti-pattern”).
The alternative to the Service Locator is the Dependency Injection pattern. It can be implemented as follows:
class StoryController extends GetxConroller {
StoryService service;
StoryController({required this.service});
void clap (times) {
service.clap(times);
}
}
final StoryController controller = StoryController(StoryServiceFirebase());
We have injected StoryServiceFirebase through the constructor. Problem solved — StoryController doesn’t know about StoryService implementation anymore.
In theory, Dependency Injection is considered a better solution than a Service Locator since it doesn’t introduce new problems.
But, practically, use whatever makes you comfortable and your project run.
In a nutshell
DIP (Dependency Inversion Principle) is a software design principle that tells us how to decouple objects.
DIP and IoC (Inversion of Control) are synonyms. DIP is more general while IoC is usually found in an “IoC container”.
Dependency Injection and Service Locator are different design patterns used to decouple objects.
Subscribe to my newsletter
Read articles from Yuriy Novikov directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by