Dependency injection and the dependency inversion principle are NOT ONE AND SAME

Translation of the article prepared in advance of the start of the course “C ++ Developer”.

Once upon a time, when I was just starting a blog on LosTechies, I wrote some articles about Dependency Inversion (DI), implementation (or injection) of dependencies (Dependency Injection – also DI) and, of course, about how I finally started to understand the essence of dependency injection. At that time, I thought that both DIs were just variations of the name of the same technique — whether you called it “injection” or “inversion”.

Injection! = Inversion

About a year after these two publications I wrote an article for Code Magazine on the principles of SOLID software development. In the process of writing, I asked my then employee Derek Greer Be a reviewer of this article. It turned out to be the best thing I’ve ever done for my understanding of SOLID, because Derek was kind enough (and patient with my stubbornness) to show me where my understanding of dependency inversion was different from the right one. He took the time to guide me through Uncle Bob’s original articles, explain where I mixed dependency injection with inversion, and finally clarified this topic for me. As a result, I got a great article, which is still quite popular – and I am very grateful to Derek for help in correcting my understanding.

However, to this day I continue to see how other people make the same mistake – even in ruby. Therefore, to help others understand what dependency inversion is and what it is not, I am republishing a section about it here from my SOLID article. I understand that part of the text will not make sense outside the context of the rest of the article. However, the general principle should be obvious, and you have the opportunity at any time to follow the link to the article to clarify your points.

(Legal formality: the following text originally appeared in the journal issue CODE Magazine for January / February 2010 and reproduced here with their permission)

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) consists of two parts:

  • Upper level modules should not be dependent on lower level modules. Both those and others should depend on abstractions.
  • Abstractions should not depend on the details. Details should depend on abstractions.

Refresh the last time you wanted to turn on the lamp to illuminate part of the room. Was it necessary to cut a hole in the wall, dig around in search of wiring, expose it and solder the lamp directly into the wiring of the house? Of course not (at least I hope not!). An electrical outlet provides a standard interface for this. No one in most industrialized countries expects a conventional lamp to solder it directly into a building’s electrical wiring. In addition, no one expects to be able to insert only one lamp into the outlet. We are used to connecting lamps, computers, televisions, vacuum cleaners and other devices. A standard 120 volt, 60 hertz electrical outlet has become a ubiquitous part of society in the United States.

The same principle applies in software development. Instead of working with a set of classes that are tightly interconnected, you would like to work with a standard interface. Moreover, you would like to guarantee that you can replace the implementation without violating the expectations of this interface, according to the LSP (Liskov Substitution Principle). Thus, if you work with an interface and want to be able to replace it, you need to make sure that you work only with the interface, and not with a specific implementation. That is, code that relies on an interface should know the details only about that interface. He should not be aware of any specific classes that implement this interface.

Relationships of Policies, Details, and Abstractions

Paraphrasing the principle of inversion a little, we can say that the policy (high level) should not depend on the detail (implementation), and the detail should depend on the policy. A higher-level policy should define the abstraction to which it will refer, where some implementation of the part performs the requested action. This perspective helps illustrate why this principle is precisely inversions dependencies, not just the principle of abstraction of dependency.

As an example of why a part’s dependency on a policy is an inverse of a dependency, consider the code we wrote in FormatReaderService. The formatted file reader service is a policy. It defines what the IFileFormatReader interface should do — the expected behavior of these methods. This allows you to focus on the policy itself, determining how the reading service works, without taking into account the implementation of details – readers of individual formats. Thus, readers are dependent on the abstraction provided by the reader service. In the end, both the service and individual readers depend on the abstraction of the format reader interface.

Reducing connectivity by inverting dependencies

You know that having no dependencies (having zero connectivity) is impractical for a class. You would not have a usable set of classes if you had zero connectivity. However, you would also like to get rid of direct connectedness whenever possible. You would like to divide your system so that it allows you to change its individual parts without changing anything other than these very parts. The principle of dependency inversion is the key to this concept. Relying only on an abstraction, such as an interface or a base class, you can reduce the connectivity between different parts of your system. This allows you to rebuild the system with various implementations.

Consider the many classes that need to be instantiated in a clear hierarchy so that you can get some of the functionality you need. The easiest way is to create an instance of the class one level lower using the class of the highest level (the one you want to call), using it to instantiate the class even lower in level, and so on. Figure 14 shows a graph of objects in which an object of a higher level (policy) depends and is directly connected with an object of a lower level (detail).

Figure 14: Policy associated with the part.

This implements the necessary hierarchy, but directly binds classes. You cannot use Foo without dragging Bar. If you want to separate these classes, you can easily enter an interface that Bar will implement, and on which Foo will depend. Figure 15 shows a simple IBar interface that you can create based on the public API of the Bar class.

Figure 15: Separation using abstraction.

In this scenario, you can separate the implementation of Bar from its use in Foo by introducing an interface. However, you only separate the implementation by selecting the interface from it. You have not yet inverted the dependency structure and have not fixed all the connectivity problems in this schema.

What happens when in this scenario you want to change Bar? Depending on what changes you want to make, you can provoke a chain reaction that forces you to change the IBar interface. Foo depends on the IBar interface, so you must also change the implementation of Foo. You may have separated the implementation of Bar, but left Foo dependent on changes to Bar. That is, the policy still depends on the details.

If you want to invert the structure of dependencies so that the Part becomes dependent on the Policy, then first of all you must change your perspective. The developer working with this system should understand that you have not only abstracted the implementation from the interface. Yes, this separation is necessary, but not enough. You need to understand who the abstraction belongs to – Politics or Details.

The dependency inversion principle states that details must be dependent on policies. This means that you must have a policy that defines and owns the abstraction that the part implements. In the script Foo-> IBar-> Bar, you need to consider IBar as part of Foo, and not just as a wrapper for Bar. Although nothing has changed structurally, the outlook for ownership has changed, as shown in Figure 16.

Figure 16: Policy owns abstraction. Detail depends on the policy.

If Foo has an IBar abstraction, you can put these two constructs in a package independent of Bar. You can put them in your own namespace, in your own assembly, etc. This can greatly increase the visibility of which class or module depends on which one. If you see that AssemblyA contains Foo and IBar, and AssemblyB provides the implementation of IBar, then it’s easier for you to notice that the granularity of Bar depends on the policy defined by Foo.

If you correctly invert the structure of dependencies, the chain reaction to changes in policy and / or details will now also be correct. When you change the implementation of Bar, you no longer observe an upward chain of changes. This is because Bar needs to conform to the abstraction provided by Foo – the details no longer dictate a change in policy. Then, when you change the needs of Foo, causing changes in the IBar interface, your changes will spread throughout the structure. Bar (detail) will be forced to change depending on policy changes.

Separation and inversion of email sending system dependencies

Looking through our code base, you may notice that IFileFormatReader is already an example of dependency inversion. The FormatReaderService class owns a format reader interface definition. If the needs of the formatted file reader service change, you will most likely see a chain of changes descending to format readers. However, if the reader of a specific file format changes, you are unlikely to see that these changes will affect the reading service of formatted files. This makes you wonder where else you can invert system dependencies.

The first thing you want to do is to separate the logic of receiving log messages and sending it by e-mail from the sending form. You do not mind the links to the two email reading and sending services, but the question of when and what to cause raises doubts. You realize that the process is actually duplicated in the form: once for sending from a file and once for sending from a database. And then you recall all the other departments that also use this, and start to wonder about the real extent of the duplication process. In addition, some of your friends recently talked about “unit testing.” They say that you must make sure that the real logic of the process you are testing is encapsulated in objects that do not have references to external systems.

And with that all in mind, you decide to create an object called ProcessingService. After a few minutes of shuffling the code back and forth to try to consolidate the process, you realize that you do not want the processing service to be directly connected to the database reader or file read services. After a little thought, you notice a pattern between them: the GetMessageBody method. Using this method as a basis, you create a new IMessageInfoRetriever interface and implement it both as a database reader and as a file reader.

public interface IMessageInfoRetriever
  string GetMessageBody(); 

public class FileReaderService: IMessageInfoRetriever
  public string GetMessageBody() {/* ... */}

public class DatabaseReaderService: IMessageInfoRetriever
  public string GetMessageBody() {/* ... */}

This interface allows you to provide any implementation for processing services that you need. Then you pay attention to the email service, which is currently directly linked to the processing service. The simple IEmailService interface solves this problem. Figure 17 shows the resulting structure.

Figure 17: Inverting the dependencies of the processing service and the file reading service.

Passing message retrieval interfaces and e-mail services to the processing service ensures that there is an instance of any class that implements these interfaces, without the need to know about specific types of instances.

Learn more about the course.

Similar Posts

Leave a Reply