Welcome to the final segment of the SOLID Principles for Android Developer series. We’ve made it to the end, and today I’m going to cover the last letter the SOLID pneumonic acronym, D: The Dependency Inversion Principle (DIP).
If you missed the first four articles, you can easily catch up here:
- S: Single Responsibility Principle
- O: Open/Closed Principle
- L: Liskov Substitution Principle
- I: Interface Segregation Principle
- D: Dependency Inversion Princple (this article)
So without further ado, our fifth and final principle -
The Dependency Inversion Principle states that we as developers should follow two pieces of advice:
a. High-level modules should not depend on low-level modules. Both should depend on abstractions.
and
b. Abstractions should not depend on details. Details should depend on abstractions.
Put succinctly, the Dependency Inversion Principle basically says this:
Depend on Abstractions. Do not depend on concretions.
Migrating to support the Dependency Inversion Principle
In order to fully grok what this principle is dictating I feel that it’s important to talk about how much of software is built – using a traditional layered pattern. We’ll look at this traditional layered architecture and then talk about how we can make changes to it so that we can support the DIP.
In a traditional layered pattern software architecture design, higher level modules depend on lower level modules to do their job. For example, here’s a very common layered architecture that you may have seen (or may even have in your application now):
Android UI → Business Rules → Data Layer
In the diagram above there are three layers. The UI Layer (in this case, the Android UI) - this is where all of our UI widgets, lists, text views, animations and anything Android UI-related lives. Next, there is the business layer. In this layer, common business rules are implemented to support the core application functionality. This is sometimes also known as a “Domain Layer” or “Service Layer.” Finally, there is the Data Layer where all the data for the application resides. The data can be in a database, an API, flat files, etc - it’s just a layer whose sole responsibility is to store and retrieve data.
Let’s assume that we have an expense tracking application that allows users to track their expenses. Given the traditional model above, when a user creates a new expense we would have three different operations happening.
- UI Layer: Allows user to enter data.
- Business Layer: Verifies that entered data matches a set of business rules.
- Data Layer: Allows for persistent storage of the expense data.
In regards to code, this might look like this:
// In the Android UI layer
findViewById(R.id.save_expense).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
ExpenseModel expense = //... create the model from the view values
BusinessLayer bl = new BusinessLayer();
if (bl.isValid(expense)) {
// Woo hoo! Save it and Continue to next screen/etc
} else {
Toast.makeText(context, "Shucks, couldnt save expense. Erorr: " + bl.getValidationErrorFor(expense), Toast.LENGTH_SHORT).show();
}
}
});
In the business layer we might have some code that resembles this pseudo code:
// in the business layer, return an ID of the expense
public int saveExpense(Expense expense) {
// ... some code to check for validity ... then save
// ... do some other logic, like check for duplicates/etc
DataLayer dl = new DataLayer();
return dl.insert(expense);
}
The problem with the above code is that it breaks the Dependency Inversion Principle - namely item (a) from above: High-level modules should not depend on low-level modules. Both should depend on abstractions. The UI is depending upon a concrete instance of the business layer with this line:
BusinessLayer bl = new BusinessLayer();
This forever ties the Android UI layer to the business layer, and the UI layer won’t be able to do its job without the business layer.
The business layer also violates DIP, because it is depending upon a concrete implementation of the data layer with this line:
DataLayer dl = new DataLayer();
How would one go about breaking this dependency chain? If the higher-level modules should not depend on lower-level modules then how can an app do its job?
We definitely don’t want a simple monolith class that does everything. Remember, we still want to adhere to the first SOLID principle too - the Single Responsibility Principle.
Thankfully we can rely on abstractions to help implement these small seams in the application. These seams are the abstractions that allow us to implement the Dependency Inversion Principle. Changing your application from a traditional layered implementation to a dependency inverted architecture is done through a process known as Ownership Inversion.
Implementing Ownership Inversion
Ownership inversion does not mean to flip this on its head. We definitely don’t want lower-level modules depending on higher-level modules either. We need to invert this relationship completely, from both ends.
How can this be done? With abstractions.
With the Java language, there are a couple ways we can create abstractions, such as abstract classes or interfaces. I prefer to use interfaces because it creates a clean seam between application layers. An interface is simply a contract that informs the consumer of the interface of all the possible operations an implementor may have.
This allows each layer to rely on an interface, which is an abstraction, rather than a concrete implementation (aka: a concretion).
Implementing this is fairly easy in Android Studio. Let’s assume that you have That DataLayer class and it looks like this:
Since we want to depend an abstraction, we need to extract an interface off of the class. You can do that like this:
Now you have an interface you can use to depend on! However, it still needs to be utilized because the business layer still depends on the concrete data layer. Going back to the business layer, you can change that code to have the dependency injected through the constructor like this:
public class BusinessLayer {
private IDataLayer dataLayer;
public BusinessLayer(IDataLayer dataLayer) {
this.dataLayer = dataLayer;
}
// in the business layer, return an ID of the expense
public int saveExpense(Expense expense) {
// ... some code to check for validity ... then save
// ... do some other logic, like check for duplicates/etc
return dataLayer.insert(expense);
}
}
The business layer now depends upon an abstraction - the IDataLayer
interface. The data layer is now injected via the constructor via what is known as “Constructor Injection”.
In plain English this says “In order to create a BusinessLayer object, it will create an object that implements IDataLayer. It does not care who implements it, it just needs an object that implements that interface.”
So where does this data layer come from? Well, it comes from whoever creates the Business Layer object. In this case, it would be the Android UI. However, we know that our previous example illustrates that the Android UI is tightly coupled to the business layer because it is creating a new instance. We need the business layer to also be an abstraction.
At this point I would perform the same Refactor–>Extract–>Extract Interface steps that I did in the prior example. This would create a IBusinessLayer
interface that my Android UI could rely on, like this:
// This could be a fragment too ...
public class MainActivity extends AppCompatActivity {
IBusinessLayer businessLayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
Finally, our higher-level modules are relying on abstractions (interfaces). Furthermore, our abstractions are not depending on details, they’re also depending upon abstractions.
Remember, the UI layer is depending upon the business layer interface, and the business layer interface is depending on the data layer interface. Abstractions everywhere!
Wiring it together in Android
Herein lies the rub. There’s always an entry point to an application or screen. In Android, that’s typically the Activity or Fragment class (the Application object is not a valid use case here because we may only want our objects to be active during a particular screen session). You’re probably wondering - How do I rely on an abstraction in the Android UI layer if this is the top layer?
Well, there are a couple ways you can solve it in Android using a creational pattern such as the factory or factory method pattern, or a dependency injection framework.
I personally recommend using a dependency injection framework to help you create these objects so you don’t have to manually create them. This will allow you to write code that looks like this:
public class MainActivity extends AppCompatActivity {
@Inject IBusinessLayer businessLayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// businessLayer is null at this point, can't use it.
getInjector().inject(this); // this does the injection
// businessLayer field is now injected valid and NOT NULL, we can use it
// do something with the business layer ...
businessLayer.foo()
}
}
I personally recommend using Dagger as your dependency injection framework. There are various tutorials and video lessons on how to set up dagger so you can implement dependency injection in your application.
If you don’t use a creational pattern or dependency injection framework you’ll be left writing code that looks like this:
public class MainActivity extends AppCompatActivity {
IBusinessLayer businessLayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
businessLayer = new BusinessLayer(new DataLayer());
businessLayer.foo()
}
}
While this may not look too bad at this time, you’ll eventually find that your object graph will grow to be quite large, and instantiating objects this way is very error-prone and breaks many of the SOLID principles. Plus, it makes your application more brittle as changes to the code can wreak havoc on your app. Ultimately, without a creational pattern or dependency injection framework, your Android UI still will not adhere to the DIP.
Patterns for Separating Interfaces
There are two patterns for separating interfaces. Which one you prefer is up to you.
- Keeping the interfaces close to the classes that implement them.
- Moving the interfaces to their own package.
The benefit of keeping them close to the classes that implement them is the pure simplicity of it. It’s not complicated, and it’s easy to grok. This has a downside though if you need to do some advanced tooling around your interfaces and implementations or if you need to share these interfaces.
The second method is to pull all of your interface abstractions into their own package and have your implementors reference this package to gain access to the interfaces. The pro of this is that it gives you more flexibility, but along with it comes the con of having another package to maintain and possibly another java module (if you’ve taken it that far). This also increases the complexity. However, sometimes this is needed due to the circumstances of how the app (and its other related dependencies) is built.
Conclusion
The Dependency Inversion Principle is the very first principle I tend to heavily rely on in every single application I write. In every app I develop I end up using a dependency injection framework, such as Dagger, to help create and manage the object lifecycles. Depending on abstractions allows me to create application code that is decoupled, easier to test, more maintainable and enjoyable to work with (this last one is key to your sanity).
I highly recommend (and I mean it with every ounce of my being) that you take the time to learn a tool like Dagger so that you can apply it in your application. Once you fully grok what a tool like Dagger can do for you (or even a creational pattern), then you can truly grasp the power of the Dependency Inversion Principle.
Once you cross the chasm of dependency injection and dependency inversion you’ll wonder how you were ever able to get by without them.
Receive news and updates from Realm straight to your inbox