Photo by Christina @ wocintechchat.com on Unsplash
Directed Acyclic Graphs: DI (Day 3)
When Math meets Android
In our previous explorations, we've unraveled the concept of Dependency Injection (DI), a powerful technique that allows the creation and management of a class's dependencies from outside the class itself. With tools like Dagger, this process is automated, streamlining both constructor and field injection.
However, as we venture further into the intricacies of DI, a pivotal question emerges: How does it all work, especially in complex projects with numerous dependencies? How does Dagger discern when to create a specific dependency and oversee its lifecycle?
To shed light on these questions, we must first delve into a foundational mathematical concept known as Directed Acyclic Graphs (DAGs). This concept not only underpins the mechanics of DI but also provides a clear roadmap for understanding how dependencies are orchestrated.
Join me as we embark on this fascinating journey, bridging the worlds of mathematics and programming, to uncover the hidden mechanisms that make Dependency Injection a cornerstone of modern software development.
What is a Directed Acyclic Graph (DAG)?
A Directed Acyclic Graph (DAG) is a graph that is directed and without cycles. Here's what that means:
Directed: The connections between the points (called nodes) have a direction. You can think of them as one-way streets.
Acyclic: There are no cycles, meaning you can't start at one node and follow the connections to end up back at the same node.
Here's a simple example of a DAG:
2: What are Edges, Nodes & Vertices in DAG?
Nodes: These are the individual points or dots in the graph. In the example above, nodes are represented by letters like A, B, C, etc.
Edges: These are the lines that connect the nodes, showing the relationship between them. In a DAG, these edges are directed, meaning they have a start and an endpoint. In the example, the arrows represent the edges.
In the given example:
Nodes/Vertices: A, B, C, D, E, F, G
Edges: The arrows connecting the nodes, like A -> B, B -> C, etc.
The structure ensures that there are no loops, and you can't follow the arrows to end up back at a node you've already visited. This makes DAGs useful in various applications like scheduling tasks, data processing, and more.
How Dagger2 uses DAG?
You might have guessed by now how DAG and DAGger are related. Obviously, the name Dagger is inspired by DAG. Dagger2 creates similar DAGs for our application during compiling and then uses it to decide when to create and provide dependencies.
Remember all those Component classes we created and marked variables and constructors with @Inject? All that was done to help Dagger2 understand how to create a similar directed acyclic graph for our own application.
Let's try to understand how the DAG might look at our application:
To recap, we created an application where the dependencies for various components looked like this:
From the @Inject annotations and the Component class we created, we tell Dagger2 two things.
Which members and constructors should be considered for creating the dagger graph?
What should be the entry point for the graph, i.e. where does the graph start?
Now, when we build our project, dagger automatically creates the dependency graph first, which will look something like this for our application
The diagram represents the dependencies between different components of the application:
Application: The root of the graph, represents the application itself.
Activity: The activity that is created by the application.
ComputeLayer: A class that the activity depends on.
NetworkSetup: The base dependency that
ComputeLayer
relies on.
How Dagger2 Created the DAG
Dagger2 uses the following steps to create this DAG:
Analyze Dependencies: Dagger2 analyzes the dependencies between different components (e.g., classes, methods) within the application.
Create Nodes: Each component becomes a node in the graph. In this case, the nodes are
Activity
,ComputeLayer
, andNetworkSetup
.Define Edges: Dagger2 defines directed edges between nodes based on their dependencies. An edge from
A
toB
means thatA
depends onB
.Ensure Acyclicity: Dagger2 ensures no cycles in the graph, making it a Directional Acyclic Graph (DAG). This ensures that there are no circular dependencies, which would lead to a compilation error.
Generate Code: Based on the DAG, Dagger2 generates code to provide the required dependencies at runtime. This includes creating instances of classes and injecting them where needed.
Initialization Order: Dagger2 uses the DAG to determine the order in which dependencies must be initialized. In this case, the order is
NetworkSetup
,ComputeLayer
,Activity
.Injection: Finally, Dagger2 uses the generated code to inject the dependencies into the target classes at runtime, ensuring everything is wired correctly.
Let's Validate :
Let's see if the concepts we learned actually work in the real world. One of the simplest ways to verify this would be to add logs to all the constructors of our classes, and then monitor the order in which these logs are printed.
Here are the updated logs in the constructors and the onCreate method of the Activity
@Inject
NetworkSetup(){
try {
Log.e(TAG, "Initialising network" );
Thread.sleep(6000);
Log.e(TAG,"Network initialization done");
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e(TAG, "Network Layer Created"); //We are tracking this log
}
@Inject
ComputeLayer(NetworkSetup networkSetup){
this.network = networkSetup;
Log.e(TAG, "Compute Layer Created"); //We are tracking this log
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
calculate = findViewById(R.id.calculate_sum)
val component = DaggerComputeComponent.create()
computation = component.computeLayer
// networkSetup = NetworkSetup()
// computation = ComputeLayer(networkSetup)
calculate?.setOnClickListener {
computation?.add(1,1)
Log.e(TAG, "Button click detected")
}
Log.e(TAG, "Main Activity Created") //We are tracking this log
}
Let's launch our app after building it and check out the logs.
Result :
And just as expected, the network layer was the first to be created and the Main Activity was the last.
But Why?
The reason why Dagger2 chooses to create the objects in the reverse order of the graph is quite simple.
Let us suppose a case scenario where MainActivity gets created before the ComputeLayer, you can imagine that then MainActivity could try to access an object of ComputeLayer that hasn't been created yet which will obviously throw a null pointer exception!
Dagger2 makes sure that all the objects are created and ready to be used before the classes that might demand them, and directed graphs are simply a way of tracking those dependencies.
Visualizing Dependency Graphs:
Keeping track of such graphs can get daunting for larger projects with multiple layers, this is where tools like Scabbard come into play.
As mentioned on the website "Scabbard is a tool to visualize and understand your Dagger 2 dependency graph."
Let's try to implement it in our own project and see if it returns the same dependency graph.
Step 0:
Install Graphviz, (the process might depend upon the OS), visit the Scabbard website for a detailed overview.
Step 1:
Add this to your dependencies section in Gradle.
//visualize dependency graph
implementation("com.github.kittinunf.result:result:3.0.0")
Step 2:
Add jitpack.io to your settings.gradle file.
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
Step 3:
Add this to your build.gradle file
scabbard {
enabled true
}
And that's it, if you build your project now, you should see a folder named Scabbard like this in your project structure.
This folder contains the png files for the graphs of your application. Here's what it generated for our app:
Notice that Scabbard was able to create the graph even before we run our application! This is because Dagger2 creates the DAG and validates it during the compile time itself.
This might look fairly simple right now, but it gets helpful as the project size increases. For e.g below is a dependency graph for another project which I've been working on.
As we progress further, we'll try to understand such graphs with all its components as well, so don't worry much about it for now.
Conclusion
We've journeyed through the mathematical concept of DAGs, understanding their structure, and then delved into how Dagger2 applies this concept to create and manage dependencies within an application. By visualizing the dependency graph, we've seen how Dagger2 ensures the correct initialization order, preventing potential errors like null pointer exceptions.
Tools like Scabbard further simplify the visualization of complex dependency graphs, providing invaluable insights as projects grow in complexity. This understanding of DAGs and their application in DI not only enhances our appreciation of modern software development techniques but also equips us with the knowledge to build more robust and maintainable systems.
As we continue to explore and innovate, the fusion of mathematical concepts with programming paradigms like DI demonstrates the limitless potential of technology. It's a testament to how interdisciplinary knowledge can lead to more efficient and elegant solutions, bridging gaps and forging new paths in the ever-evolving landscape of software engineering.