The Dependency Inversion Principle

The Dependency Inversion Principle

On the previous articles I have presented the SOLID principles as a guide to using the concepts of object-oriented programming, concepts such as "encapsulation", "inheritance" and "polymorphism" (to these I would also add the "aggregation") (1.)

Let's summarize what we have seen so far:

We first saw the Single Responsibility Principle. SRP drives us to create cohesive classes that are easy to use and to reuse.

Then we saw the Open Closed Principle. OCP helps us to make classes that are easy to extend but closed to modifications, we saw that inheritance and aggregation help us to achieve this goal.

After we saw the Liskow Substitution Principle. LSP drives us on the correct use of inheritance which is the strongest constraint in object-oriented programming.

And last time we saw the Segregation Interface Principle. SIP is at the basis of a correct design of interfaces and abstract classes.

So what are we still missing? we still lack a principle to help us manage the dependencies between components.

Here it is the the fifth SOLID principle, “Dependency Inversion Principle” represented by the letter 'D' in the acronym 'SOLID' and abbreviated with DIP.(2.)

Robert Martin presents his last principle with these two sentences.

?"High-level modules should not depend on low-level modules. Both should depend on?abstractions"

"Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions"

?The general idea behind this principle is as simple as it is important: High-level modules, which provide complex logic, should be easily reusable and unaffected by changes in low-level modules, which provide utility features. To achieve that, you need to introduce an abstraction that decouples the high-level and low-level modules from each other.

Let's try to clarify what is meant by high-level module and low-level module with an example: imagine having to develop a software component for Android smartphones that helps the user to maintain the right heart rate during a run. This component uses some biometric data set (such as body weight, age and maximum heart rate), uses the heart rate data detected and according to the goal that the user wants to achieve (weight loss or aerobic performance) will provide the user with the indications to adapt your running intensity in order to reach the chosen goal. For our discussion it is not essential to know the strategy that the component will have to implement, it is however a feedback control model set on maintaining the correct cardio zone. I remember anyone unfamiliar with exercise that cardio training zones are intervals within which the heart rate must be in order to get the maximum benefit from training.

We will call this component "HeartRateMate".

"HeartRateMate" will have to acquire the heart rate from a Bluetooth LE cardio sensor, then will have to send the acquired data to an application (already installed in the system) which will present the data on the screen (this application requires that the data be in XML). The module will also have to manage a database on which to save the data acquired during the exercise.

Let's see the design in the UML diagram below.

Non è stato fornito nessun testo alternativo per questa immagine

Let's remember that the arrow in UML diagram should be read as "depends on".

In this design, we could identify "HeartRateMate" as a high-level module, living in context as a foreground service, one that the Android (operating system) usually doesn't kill when there is a shortage of memory.

Low-level modules are those that directly use the Android API or other Third Parts libraries, for example XML Document API, Bluetooth API, SQLite API or Room Library.

?Now let's remember what the Dependency Inversion Principle says: high-level modules shouldn't depend on low-level modules. Both should depend on abstraction.

Instead in our design there are some strong dependences between the high level class ("HearRateMate") and the lower level classes ("BtleHrReceiver", "HrXMLFormatter" and "SQLDatabase").

So let's try to apply the following transformation:

-) create an interface for each low-level components and apply inheritance between these components and the interface

-) eliminate the associations between the high-level component and the low-level components and replace them with the associations between the high-level component and the interfaces.

This transformation is called "Dependency Inversion" (DI), because it reverses the dependency, now the strong association (an Interface Realization relationship) is between the abstractions (interfaces), usually very stable and the low level concrete components more unstable (we not be surprised if we are asked to integrate the ANT + cardioreceiver or if we are asked to format the heart rate data in "json" instead of "XML", or if we are asked to replace the relational database with a much more fast).

Now the association between the high-level component and the low-level components has been removed and has been replaced by the association between the high-level component and some stable abstractions. Let's see the design below.

Non è stato fornito nessun testo alternativo per questa immagine

As we can see, now there is a decoupling layer between the high-level and low-level modules, so the first DIP sentence "High-level modules should not depend on low-level modules. Both should depend on abstractions" are respected and we have achieve a good design.

We could say that every change to an abstract interface corresponds to a change to its concrete implementations. Instead, changes to concrete implementations do not almost never require changes to the interfaces that they implement.

Therefore interfaces are less volatile than implementations.

Indeed, good software designers and architects work hard to reduce the volatility of interfaces. They try to find ways to add functionality to implementations without making changes to the interfaces.

The implication, then, is that stable software architectures are those that avoid depending on volatile concrete classes, and that favor the use of stable abstract interfaces. This implication leads to a pair of very specific coding practices:

1) Don’t refer to volatile concrete classes. Refer to abstract interfaces instead.

This rule applies in all languages, whether statically (c++, java, kotlin, c#, etc..) or dynamically typed (python, JavaScript, etc..). It also puts severe constraints on the creation of objects and generally enforces the use of "Abstract Factory", "Factory Method" or "Dependency Injction" (3.).

1) Don’t derive from instable concrete classes. In statically typed languages, inheritance is the strongest, and most rigid, of all the source code relationships, consequently, it should be used with great care. In dynamically typed languages, inheritance is less of a problem, but it is still a dependency—and caution is always the wisest choice.

?And now let's see another interesting example of DIP violation, ?we consider an eCommerce Web Application.

The snippets UML diagram below shows a very high level design model for eCommerce Web Application.

Non è stato fornito nessun testo alternativo per questa immagine

There are 3 major business functions - "ProductCatalog", "PaymentProcessor" and "CustomerProfile".

These, in turn, depend on a number of other modules , shown below, for the implementation.

The modules on top are closer to the business functions so they are the high level modules.

The modules towards the bottom deal with the implementation details and are the low level modules.So the bottom modules are "SQLProductRepository", "GooglePay", "WireTransfer", "EmailSender" and "VoiceDialer". These deal with the low-level implementation.

How about the "Communicator" module? Is it a high level module or a low level module?

Well, the answer is : Its both!

If you consider the "CustomerProfile" and the "Communicator" module alone, "CustomerProfile" is the high level module and "Communication" is the low level module.

But if you consider the "Communicator" and the "EmailSender" module alone, "Communicator" is the high level module here and "EmailSender" is the low level module. The point here is that whether a module is a high level or low level module is relative and not absolute.

?We could see that "ProductCatalog" , a high level module, depends on, "SQLProductRepository", a low level module. So this directly conflicts with DIP.

?The definition says "High Level Modules should NOT depend on Low Level Modules. Both of them should depend on abstractions instead".

Let’s take this particular relationship alone and analyze it in depth, let’s see some code snippets for this relation (snippet code below).

Non è stato fornito nessun testo alternativo per questa immagine

So we have a "SQLProductRepository" class which contains a method called "getAllProductNames()". Assume this method contains the code to run a SELECT statement from a PRODUCT database table in the backend. This method returns a list of product name strings.

Next, we have the "ProductCatalog" class. Here we first instantiate a new "SQLProductRepository" object, and then we call the "getAllProductNames()" function on the instantiated object.

So "ProductCatalog" directly depends on "SQLProductRepository", so this is the violation of the principle as seen in code.

Now let's see if we can fix this violation.

We will create an interface, an interface is an abstraction, and we'll call it "ProductRepository".

We will make the "SQLProductRepository" implement this interface.

Let’s see the snippet code below.

Non è stato fornito nessun testo alternativo per questa immagine

As we can see we use a Factory class in order to get the "SQLProductRepository" object to the "ProductCatalog" (3.).

The Factory class has a single method called create which instantiates and returns a new "SQLProductRepository" object.

From the "ProductCatalog" class, we invoke this factory method which instantiate and return a "SQLProductRepository" object.

So now so we don't have any tight coupling between with "SQLProductRepository" anywhere in the "ProductCatalog" but note that our reference object is "ProductRepository" that is an abstraction (interface).

So "ProductCatalog", which is our high level module, depends on "ProductRepository", which is an abstraction.

Next, "SQLProductRepository" , which is our low level module, depends on "ProductRepository" again, because it implements it.

As you can see now, we are no longer violating the DIP principle.

The high level module does not depend on the low level module anymore but both high level and low level modules depend on the abstraction.

So now, the detail, which is the "SQLProductRepository" class, is dependent on the abstraction, which is the "ProductRepository" class, the initial dependency from "SQLProductRepository" changing direction, and this is the reason why this principle is called 'dependency inversion principle'.

If we apply the same method also to the other two high-level and low-level modules that are in the design then we obtain the design shown below

Non è stato fornito nessun testo alternativo per questa immagine

?Let’s conclude this example by talking about another concept strictly connected to DIP, the “dependency injection”.

To understand dependency injection, let's go back to the “factory method” pattern that we have implemented in order to get an instance of the "SQLProductRepository" object.

Even though this delegates the responsibility of instantiation to a different factory class, still the responsibility for triggering this instantiation process is with the "ProductCatalog" class.

?If we don't want the "ProductCatalog" to worry about how and when to trigger the instantiation, we have to use the “Dependency Injection” pattern (3.).

Lets see how that looks in code snippet below.

Non è stato fornito nessun testo alternativo per questa immagine

We have a constructor now, for the "ProductCatalog" class, which takes in a "ProductRepository" object. We also have a class with a main method that creates and makes use of the "ProductCatalog" class. It will first call the factory method, get the instantiated "SQLProductRepository" object, and then invoke the constructor on the "ProductCatalog" class by passing on the newly instantiated object.

So now, when "ProductCatalog" is created, it gets a "SQLProductRepository" object. "ProductCatalog" is free to use the "SQLProductRepository" object whereever and whenever it wants.

"ProductCatalog" does NOT need to worry about any instantiation.

In other words, we are injecting the dependency into "ProductCatalog", instead of "ProductCatalog" worrying about instantiating the dependency.

This is the concept of dependency injection, it is a pattern that belongs to the category of creational patterns, we will see it in a few episodes, when we talk about creational patterns.

So to sum up in all the examples we have seen, we started with showing a violation of the dependency inversion principle. We understood what the violation is, then we took steps to restructure the code, so as to strictly follow this principle.

The main concepts we saw in this short article are three:

-) the Dependency Inversion Principle definition, with its two statements.

-) the Inversion of Dependency Techniques (DI)

-) a pair of creational patterns : factory class e dependency injection which must be applied in conjunction with the Dependency Inversion.

But after more than thirty years of experience in design and implementation of object-oriented software, let me say that the problem of associations between classes is much large than that covered by Dependency Inversion Principle.

In the next article I want to try to integrate the SOLID principles with some additional rules and with some transformation techniques that allow us to deal with all the associative anomalies between classes, including usage dependencies, creational dependencies and cyclical dependencies.

See you soon.


I remind you my previous article:

thanks for reading my article, and I hope you have found the topic useful,

?

Feel free to leave any feedback

your feedback is appreciated

?

Stefano

?

1.????James Rumbaugh, “Object-Oriented Modeling and Design with UML” Addison Wesley (November 2004)

2.???Robert C. Martin, “Clean Architecture - a craftsman's guide to software structure and design” Prentice-Hall (November 2018) p 105-112

3.????Gamma, E., Helm, R., Johnson, R. e Vlissides, J., Design Patterns -Addison-Wesley p.87-116

要查看或添加评论,请登录

Stefano Santilli的更多文章

社区洞察

其他会员也浏览了