What is Dependency Inversion Principle?
High-level layers should not depend on the low-level layers. Both should depend on an interface/protocol.
High-level module is the part of our code that we want to protect
High-level module calls the low-level module
Our domain layer where our business logic is, is part of the High-level module - this can be our interactors / use cases.
Here are some examples of when we can use Dependency Inversion:
Let's say we have this structure. This structure actually works well for "small" projects (although, that's subjective and I don't quite know yet what's considered a small project). But for the sake of having an example, and since I mentioned that our interactors are part of our Domain Layer (High-level), which is what we want to protect from changes of the low-level classes, let's do this.
If we have this structure and we change something in our DAO / Service, that will affect Repository, then we update Repository, and then we update the Interactor, it sometimes reaches the presentation layer. You'll definitely see the domino effect if you update the return, or the struct used, or the function names, the parameters, you'll have to update each layer to reflect that change.
With Dependency Inversion, we want both our High-Level and Low-Level to depend on an interface.
In this structure, the interactor only needs to care about the things it needs in order to execute what it needs to do. And those things are defined in the InteractorDataSource Interface. The interactor does not care how those things are done, it will simply call the dataSource to execute it (and it is on its own).
The Repository will implement the DataSource interface (Thank God, someone will actually do it!), but the interactor does not know that, and it doesn't care who does it, and how it's done.
Here's a code snippet of how these layers will look like:
You can also have your own struct in the interactor, a struct that may only contain the data needed to show to the UI. It is a separate struct (or response structure) from the service layer. In that way, we can protect it more from changes on the Low-level classes.
With this, if the Service/Dao changes, only the Repository will be affected.
Dependency Inversion can be a bit tricky, and in this case, there's even a "new" layer, which is the DataSource and it can be overwhelming sometimes.
Another example is when we are using third-party services for:
Push Notifications / Cloud Messaging
Analytics
Crash Reporting
Class B here implements the interfaces, and it is a class that directly calls these third-party services. Client A is what the rest of the classes use. If the team decides to change these 3rd-party services, we will only have to update Class B. It's very convenient, because we don't have to check all the classes that are calling these services and update them one by one.
With Dependency Inversion Principle, we can change the services any time without worrying if we will break other parts of the code.
Another example is when we create frameworks, or SDKs to be used in the project:
Based on the book "Dive Into Design Patterns" by refactoring.guru, there are three levels of reuse.
Frameworks - Highest Level
Design Patterns - Middle Level
Classes - Lowest Level
I have also worked on some features wherein I have designed it in such a way that if we want to put it in a separate module, or SDK, or even repository, we can do it easily. And by that, I also consider that as a high-level module. This module does not care who calls it. And the client also doesn't not care how it's done.
Edit: I just realized that this is actually called Modular Architecture. You can check more about it by searching about Monolithic vs. Modular Architecture. You may also check out Yair Carreno's "Clean Architecture on iOS" book, that's where I read about Monolithic vs. Modular Architecture. His book is pretty advanced, when I first read that part, I actually didn't understand fully what it meant, but now that I've been doing it all along and there's a proper name for that, it made me feel good that I finally understood what it meant.
If you're going to ask me what's the point in doing that? Well, our team is divided into different squads, some squads are more focused on specific parts of the app and the others are not. Let's say, Squad A has this new Feature A, but to get into Feature A, there are some Feature checks that need to be done that is from Squad B, Step 1 → Step 2 → Step 3, and if all is well, we can show Feature A to the user.
Here's a diagram to get an overview of what we're trying to implement:
The good thing about this is that Squad A doesn't need to stress themselves about these Feature checks, the client only needs to call and wait for the response to know if they can launch Feature A or not. Also, if Squad B decides to update the steps, remove, or add more, they can do it without worrying if it will affect the client. FeatureCheck is isolated from the clients.
So if your team decides to do something like that, or decides to create a framework, or SDK, we have to keep in mind that this is the highest level:
The high-level components should not depend on low-level components
The high-level components does not care about low level components
We can access high-level via its public interface, low-level components don't need to know the concrete classes behind it
High-level components will call low-level components when it is time for them to do their job
We protect high-level components from changes from the low-level components. The low-level components should not be able to do something to update/modify high-level components.
How do we use Dependency Inversion Principle to make sure our frameworks/SDKs/modules stay isolated and has no dependencies in low-level modules?
We can start with something simple and build it up depending on what we need.
We need a public interface, one that the client knows about. In this interface, we are telling the client what it can do.
Define structs for the type of results/responses we can give back to the client.
If we need something from the client side, example, we need to have an authenticated API calls, so we'll need an auth-token for our services. We declare another interface for that.
Here's some code snippets on how this setup above looks like:
Lastly, if we want to add a certain behavior, example, we want to show this particular screen first before proceeding to the next step, then we can do that, too. In that way, we let the client / app side do the UI part since it is not the framework's responsibility. We can create another interface / protocol for that.
Here are some code snippets based on the updated architecture:
This blog has gotten really long, so I'll end it here. I hope I was able to make things clearer for those who are confused with DIP.
Comments