Fundamentals of Dependency Injection

Fg. car assembly line process

Recently, I’ve finished a course “Dependency Injection in Android with Dagger 2 and Hilt” by Vasiliy Zukanov from Udemy and it is one of the best courses I’ve ever taken. He has explained everything from why do we need dependency injection to how to use Dagger and hilt for the same. He literally refactored a project from everything in an activity to providing all services using DI. This article is not about the implementation of an DI framework, it’s about my learnings and understanding about the fundamentals of DI from this course.

Consider a basic analogy for this. Suppose you want to make a sandwich. What you do is, you go to the shop and grab a few vegetables, cheese & bread and then assemble it. There you have, a nice tasty sandwich. So what you did is, you take the ingrediants from other sources (instead of making it itself) and used it as per your need. You didn’t care about how the shop has those ingrediants. And this is what we do using dependency injection. We pass the services to the clients that would use it.

The most popular and basic definition of dependency injection is, “just passing services into the clients from outside”. But is it though? Vasiliy shared a few puzzler questions to explain this and I would like to mention a few.

  1. Suppose there is a class called DataAggregator something like this:
class DataAggregator {
private final List<Data> mData;

public DataCollector(){
mData = new LinkedList<>();
}

public void addData(Data data){
mData.add(data);
}

public List<Data> getData(){
return mData;
}
}

So, would you call contructing mData(mData = new LinkedList<>(); )a violation of dependency injection because I’m creating an object of LinkedList class instead of passing it from outside? No, of course not. The first thing is, on passing an object of LinkedList , we would lose the encapsulation of the internal data of the DataAggregator class which doesn’t make sense as per the design of this class. Because, those who are using this DataAggregator class shouldn’t know about how it is maintaining the data. Only thing that other classes should know is that they can add the data and fetch the list of the data.

Another point is Lists are not object, they are data structures. Let’s consider it in context of object oriented design. Objects expose behavior and it might operate on other data structures. A simple distinction between object and data-structure is, a phone data structure has a battery, cameras, speakers etc. but a phone object takes pictures, play music etc. So, an objects acts and behaves but a data strucutre, well, it just exposes the data. So it’s not a violation of DI to construct a data structure inside a class as it’s in its fundamental design to encapsulate the data but not to expose the internal details. There is a good article about the object vs data-structures that I recommend.

2. If all clients get their services from outside then who instantiate all these services? And those who instantiates, does they violate the principles of DI? This is what Vasiliy demonstrate in the tutorials by defining the application’s logic into 2 disjoint set (construction set and functional set).

  • Functional set: It contains classes that encapsulates core application functionalities.
  • Construction set: It contains classes that deals with the wiring of the dependencies and intiate objects from the functional set.

This is most fundamental thing about dependency injection and he has explained this in more detail in his course. Over all, dependency injection is all about handling dependencies in a Large scale structure.

There are 3 fundamental ways to inject a service into a client

  • Constructor Injection: It’s basically passing the services through constructor.
  • Method Injection: Passing services using a method of the client.
  • Field Injection(property in kotlin): Providing services using public fields/properties in the client.
class Client(
private val service1: Service1 -> Constructor Injection
) {
private var service2: Service2
fun setService2(service2: Service2){ -> Method Injection
this.service2 = service2
}
var service3: Service3? = null -> property injection
}

Most of us use DI framework like Dagger, Koin, Hilt etc. for our convienience but one should also know that in case of pure dependency injection(i.e. independent of any framework), we can acheive the same using above mentioned different ways of injection.

It simply means talk to your immediate friends. Let me describe this by an example.

class LocalRepo(private val context: Context){
private val sharedPreferences = context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
}

In above example, LocalRepo class construct an object of sharepreferences using context object which is an extra dependency for it (if it is using context just for creating a sharedpreference object). This violate the law of demeter. The correct way to do the same is passing the sharedprefence object through the constructor instead of context.

class LocalRepo(private val sharedPreferences: SharedPreferences){}

That’s all folks for this article. I hope this will give you an idea about the fundamentals of dependency injection. There is also an article by Vasiliy which is kinda summary of his course and it’s worth reading it.

Happy learning! Peace out ✌️

Learning Android | Android Developer @ slice