
Why service locator is so unpopular
And how you can use it in project
Today passed over three years since dagger 2 knocked to our doors. Compile-time dependency injection framework that uses code generation to remove a huge amount of boilerplate code. Sounds impressive, right? Yeah, it was in times, where there were not so good alternatives: either Dagger 1/Guice or writing all by yourself. Considering that those times were about ruling java in the world of Android, the second alternative was a complete mess.
So many engineers used the first alternative, many didn’t use anything for injection. I like how people always say how it is bad, but let’s be honest, we can live with that and be pretty productive. Just look at sources of Telegram. Or you can just create a multimodule system with encapsulated and isolated behavior. Yeah, in those times there were modules too ;)
But there was already the third alternative. And its name is Service Locator. This pattern helped you to organize all services/handlers/managers and other -ers in one place and get access to them in any part of the application. Thus, it simplified development in one hand and had some issues (if you use canonical pattern) on another hand.
But, I ensure you, just several updates and limitations and Service Locator transforms from anti-pattern to simple way of handling dependencies + when your codebase will grow enough and you will need to really juggle dependencies, you will be able to easily transform Service Locator to Dagger module, and then split it on submodules or subcomponents
In this article we will talk about what is a Service Locator, describe its cons and will make a basic refactoring of this conception. Also, We will slightly touch some problems of dagger, kodein and why we can consider Service Locator as an alternative still nowadays at least in the begging.
What is that?
The service locator pattern describes a special object which gathers all instances of services, that can be used through the whole application. In the canonical implementation, locator is passed to the instance of object to provide services for it. Let’s look at a basic example:
Generic provider
First of all we need to remove generic fun <T> provide(clz: Class<T>): T
method, since it is not reliable and we can not guarantee which instances of which classes will be ready by that time. It is much better to declare providing each dependency through its own separate method, which guarantees to return new instance or the cached one. Because of using kotlin we can make even more nicer approach: using properties. Let’s update our example:
And if you have some external dependencies which are needed suppose for Service1
or Service2
just:
- Consider to add their declaration and creation inside
ServiceLocator
- Provide then through factories, injected through the constructor of
ServiceLocator
Testability
The main problem now as it may seemed at first glance is lack of testability of a code. You can use mockk for mocking final classes (which is ExampleClass), but it seems more the hack, rather the general ability of setting different test dependencies.
To resolve that in common case we can introduce a service locator as interface and hide the realisation:
Making implementation as a private class is not important for this, but I think that we always need to keep that rule with us: to hide something when it is not needed to be seen.
Now, if you want to test ExampleClass, you will be able to inject a special test instance of ServiceLocator
, which you will be available to make through mock libraries or just create a decorator with overriding only test instance of setvices. For instance:
class TestServiceLocator(origin: ServiceLocator) : ServiceLocator by origin {
override val service2: Service2 by lazy { /*test instance*/ }
}
Injection of needed
The other big issue here is that we pass service locator to ExampleClass, giving the possibility to use any services. When you have a big project, with a huge pile of services you will fastly notice, that you not sure which services used by which classes.
To fix that let’s transform our patter and introduce a limitation, to not pass service locator through the constructor or setters to classes. But how we will use that. Well, there will be several classes in which you can inject Locator. In our Android development case, those classes are Activities, Fragments, Views, Services, etc. all that can be created or recreated by the system. This limitation can be a contract between developers (you just agreed to have imported ServiceLocator in aforementioned classes and for instance set up linter for that), or a contract in the codebase.
Let’s introduce sample limitation through codebase. The idea is that we will have base Application
class which will be aware of AppServiceLocator
and will have possibility to built it (for example, base application class lies in same file as AppServiceLocator
). All other entities will retrieve ServiceLocator
instance through Application
instance or instances that already know, how to retrieve ServiceLocator
:
The sample usage will be next:
class SampleFragment : Fragment(), ServiceLocatorFragment {
override val self = this
private val exampleClass = ExampleClass(serviceLocator)
override fun onResume() {
super.onResume()
exampleClass.doSomething()
}
}
Now let’s remove ServiceLocator
dependency from ExampleClass
:
Now you got rid of service locator anywhere but base classes.
The result
You still not able to have an analog of modules/subcomponents and lifecycle, but instead of a complicated system with a tremendous amount of annotations that are not sharpened for Kotlin, and thus looks even more ugly (yeah, I talk about dagger 2).
Or. Instead of nice and concise Kotlin oriented DI framework (Kodein), but with runtime dependencies and a huge amount of constant strings for naming dependencies.
Instead of all of that, you will have a simple container of global instances. It still has its minuses, that’s why I recommend to use it as the first step when you need to create a for example MVP, first version. Then you will be able to easily migrate to full fledged DI framework in the future and split locator on different modules/subcomponent, etc
P.S.
I too often heard how people start to write a new project from adding dependencies. Especially rx and dagger. This article is not about that Service Locator is a silver bullet. This article is about that service locator is one of the best starting points.