What are DI benefits?, What is the difference between (DI) and Service Locator? and when not to use it?
Notes“This article is not a tutorial of how to implement or use DI in your project. Its goal is just to give an overview of DI, its benefits and when to use it.”
“For the rest of the article we will take Android/Kotlin with Dagger 2 as our reference for examples and code. But, same concept goes for any other programming language”
------------------
Dependency Injection is a software design pattern in which a component or object is provided with its dependencies, rather than hard coding them within the component or object. This helps to decouple the objects from their dependencies and makes the code more maintainable, flexible and easier to test, as the dependencies can be easily swapped out or mocked.
Examples of dependency injection frameworks include:
In Android, dependencies are often required in Activities, Fragments and other components. Without dependency injection, these dependencies would typically be created within the component’s constructor or onCreate() method.
For example, consider a simple Activity that displays a list of items from a repository:
class ItemListActivity : AppCompatActivity() { private lateinit var itemList: ListView private lateinit var itemRepository: ItemRepository override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_item_list) itemList = findViewById(R.id.item_list) itemRepository = ItemRepositoryImpl() // ItemRepositoryImpl is a concrete implementation of ItemRepository. } }
In this example, the ItemRepositoryImpl is created within the Activity’s onCreate() method. This tightly couples the Activity to the specific implementation of ItemRepository, making it difficult to test and less flexible.
To use dependency injection, we can use a library like Dagger 2 to provide the dependencies to the Activity. First, we need to define a Module that provides the dependencies:
@Module class ItemModule { @Provides fun provideItemRepository(): ItemRepository { return ItemRepositoryImpl() } }
This module defines a provideItemRepository() method that returns an instance of ItemRepositoryImpl.
Then, we need to create a Component that uses the module to provide the dependencies:
@Component(modules = [ItemModule::class]) interface ItemComponent { fun inject(activity: ItemListActivity) }
This component defines an inject() method that can be used to provide the dependencies to the Activity.
Finally, we can use the component to inject the dependencies into the Activity:
@Component(modules = [ItemModule::class]) interface ItemComponent { fun inject(activity: ItemListActivity) }
In this example, the @Inject annotation is used to indicate that the itemRepository should be provided by Dagger. The itemComponent.inject(this) line is used to instruct Dagger to provide the dependencies to the Activity.
With this setup, it’s easy to test the Activity by providing a mocked repository or by swapping out the repository implementation entirely. And it’s also easy to add more dependencies if needed and to manage all of them by modifying the ItemModule and ItemComponent class.
----------------------
// example of testing the ItemListActivity with a mocked repository class ItemListActivityTest { @Test fun test_itemList() { val activity = ItemListActivity() val mockRepo = mock<ItemRepository>() `when`(mockRepo.getItems()).thenReturn(listOf(item1, item2)) val component = DaggerItemComponent.builder() .itemModule(ItemModule(mockRepo)) .build() component.inject(activity) activity.onCreate(null) // assert that itemList contains the correct items } }
// example of providing different repository implementations based on build type class ItemModule { @Provides fun provideItemRepository(context: Context): ItemRepository { return if (BuildConfig.DEBUG) { DebugItemRepository(context) } else { ProdItemRepository(context) } } }
--------------------
Both dependency injection (DI) and service locator are patterns used to manage dependencies in software development, but they are used in slightly different ways and have different benefits and drawbacks.
Dependency injection (DI) is a pattern in which a component is provided with its dependencies through its constructor, method calls, or fields. The component itself does not need to know how to create or look up its dependencies; they are simply passed in.
Service locator is a pattern in which a component looks up its dependencies from a central registry, rather than having them passed in. The component itself needs to know how to look up its dependencies, and the registry needs to be configured with the correct implementations. This can make the code more difficult to test and less flexible, as the dependencies cannot be easily swapped out or mocked. Examples of popular service locator frameworks include Kodein, PicoContainer and Google Guice.
Another difference is that the service locator pattern is considered an anti-pattern. Because it adds a global state to the application, it makes it harder to reason about the dependencies in the code and testability.
It’s also worth noting that there is a gray area between those two patterns and it’s called Contextualized Dependency Lookup(CDI) which is a variation of service locator pattern and it’s considered a better practice than pure service locator pattern.
---------------------
Dependency injection (DI) is not always the best solution for every situation. Here are a few cases when it may not be appropriate to use DI:
These are just examples, and whether or not to use DI may depend on the specific requirements and constraints of your project. It’s important to weigh the benefits and drawbacks of using DI in each case, and to choose the best solution for the specific needs of your project.