In this third entry, we tell you about the Dependency Inversion Principle, with key concepts and day-to-day examples.
DIP: Dependency Inversion Principle
This principle proposed by Uncle Bob states:
“High-level modules should not depend on low-level modules. Both must depend on abstractions ”.
"The abstractions should not depend on the details, the details should depend on the abstractions."
But before going into detail, let's talk about some basic concepts to understand the sentences that make up the principle, we are going to iterate over an example and then we are going to apply the principle:
“High-level modules should not depend on low-level modules. level. Both must depend on abstractions.”
For software, high-level modules refer to those that are as close as possible to the functionality of the system. We can associate this with use cases, having a service that is responsible, for example, for processing order, high-level modules could be considered those that encapsulate said use case, orchestrating everything for this use case to occur.
The low-level modules are those modules that are more specific, again with the example, processing an order contains many steps internally, which is not visible to the consumer (inventory review, customer validation, promotion validation, process payment, among others), how it connects to an api, or a query service, is not important globally, but it is fundamental internally.
"The abstractions should not depend on the details, the details should depend on the abstractions"
What is a dependency?
This occurs when one class or function makes use of the methods of another.
What is coupling?
Coupling measures the level of dependence one class has on the other. In our example it would be how much a change in the Banking API will impact the controller that processes an order, this measurement is very important when diagnosing how adaptive our application would be to change, in our case, if the organization wanted to add a new one payment gateway; How expensive would it be to add such functionality? If a new change to the bench API would not impact the controller logic, then I can say that there is low coupling, a good indication of easy code to extend.
What is abstraction?
A general concept of abstraction could be that of being a concept or idea that is beyond a specific case, and which in turn can serve as a reference to group these specific cases into a category according to the abstraction.
An example of this could be that of a table:
An abstract concept of a table can be that of an element that extends vertically from the ground level, that has a flat surface whose purpose is to support things. From this abstract concept we could categorize the following as tables:
In object-oriented programming (OOP), this is achieved by creating interfaces or abstract classes since none of them is a specific case but a global definition of something.
With these clear concepts we can review our example case:
We can conclude that the dependency investment principle is not fulfilled since the high-level module (PlaceOrderController) depends directly on the implementation of the low-level module (BankAPI). Both do not depend on abstractions (they are implementations in this case), and since there are no abstractions there is a strong co-dependency on the details (If a new gateway enters, the controller must be changed almost completely).
For our case study, let's imagine that the bank's API is an external resource, that is, it cannot be changed but can be consumed. In a first step, we have to remove the dependency of the implementations, adding an interface:
Here we are abstracting the high-level module (controller) from the low-level module (banking api) and we define an interface to interact with it, that is, the controller specifies the contract that the payment gateway apis must fulfill in order to interact with the t. Now we still have one last step, which is the abstraction of the details (the payment processing by the banking API). Since the PaymentProcessor defines a new contract so that the controller can use any payment processing implementation (The bank and future gateways):
The problem now is that the Banking API is an external resource, so we cannot change it so it can implement interface pay function (part two). For this we could integrate a design pattern called Adapter, which would be to create a class that implements said interface, and makes use of the implementation of the banking api to process the payment:
And thus it is possible to decouple the dependency of the modules (controller and api) through abstractions (PaymentProcessor) and it is also avoided that the abstractions (PaymentProcessor) depend on the detail (BankAPI), if not that the detail (BankAPI) depends on the abstraction (Creating an adapter that interacts).
With this structure it will be easier, for example to implement a new payment gateway such as a third party service:
CONCLUSION
As we have seen, SOLID principles represent an important knowledge when creating clean, easy to extend and maintain code. Cases like the mentioned examples are very recurrent in the day to day of a programming professional and such practices help us to maintain an order in what we do, and thus leave an organized work, which can then be maintainable for other people, because nobody would like to start maintaining a messy and difficult to understand code. In the end, design patterns are a good way to generate empathy among professionals, since putting yourself in the shoes of those people who later come to contribute to your code will make you understand the importance of "keeping a clean house".
Disclaimer: The statements and opinions expressed in this article are those of the author(s) and do not necessarily reflect the positions of Thoughtworks.