|
From Dependency Injection Principles, Practices, and Patterns by Mark Seemann and Steven van Deursen What purpose does DI serve? DI isn’t a goal in and of itself, rather a means to an end. Ultimately, the purpose of most programming techniques is to deliver working software as efficiently as possible. One aspect of that is to write maintainable code. This article discusses what DI is (and is not).
|
Save 37% on Dependency Injection Principles, Practices, and Patterns. Just enter code fccseemann into the discount code box at checkout at manning.com.
Writing maintainable code
Unless you write prototypes or applications that never make it past release 1, you’ll soon find yourself maintaining and extending existing code bases. To be able to work effectively with such a code base, in general, the more maintainable your code is the better.
An excellent way to make code more maintainable is through loose coupling. As far back as 1995, when the Gang of Four wrote Design Patterns [1], this was already common knowledge: Program to an interface, not an implementation.
This important piece of advice isn’t the conclusion, but, rather, the premise, of Design Patterns; to wit: it appears on page 18. Loose coupling makes code extensible, and extensibility makes it maintainable. DI is nothing more than a technique that enables loose coupling. However, there are many misconceptions about DI, and sometimes they get in the way of proper understanding. Before you can learn, you must unlearn what (you think) you already know.
Common Myths About DI
There are at least four common myths about DI:
-
DI is only relevant for late binding.
-
DI is only relevant for unit testing.
-
DI is a sort of Abstract Factory on steroids.
-
DI requires a DI Container.
Although none of these myths are true, they’re prevalent nonetheless. We need to dispel them before you can start to learn about DI.
Late binding
In this context, late binding refers to the ability to replace parts of an application without recompiling the code. An application that enables third-party add-ons (such as Visual Studio) is one example. Another example is standard software that supports different runtime environments. You may have an application that can run on more than one database engine: for example, one that supports both Oracle and SQL Server. To support this feature, the rest of the application can talk to the database through an interface. The code base can provide different implementations of this interface to provide access to Oracle and SQL Server, respectively. A configuration option can be used to control which implementation should be used for a given installation.
It’s a common misconception that DI is only relevant for this sort of scenario. That’s understandable, because DI does enable this scenario, but the fallacy is to think that the relationship is symmetric. Just because DI enables late binding doesn’t mean it’s only relevant in late binding scenarios. As Figure 1 illustrates, late binding is only one of the many aspects of DI.
Figure 1. Late binding is enabled by DI, but to assume that it’s only applicable in late binding scenarios is to adopt a narrow view of a much broader vista.
If you thought that DI was only relevant for late binding scenarios, this is something you need to unlearn. DI does much more than enable late binding.
Unit testing
Some people think that DI is only relevant to support unit testing. This isn’t true either-although DI is certainly an important part of supporting unit testing. To tell you the truth, our original introduction to DI came from struggling with certain aspects of Test-Driven Development (TDD). During that time, we discovered DI and learned that other people had used it to support some of the same scenarios we were addressing.
Even if you don’t write unit tests (if you don’t, you should start now), DI is still relevant because of all the other benefits it offers. Claiming that DI is only relevant to support unit testing is like claiming that it’s only relevant for supporting late binding. Figure 2 shows that although this is a different view, it’s a view as narrow as Figure 1.
Figure 2. Perhaps you’ve been assuming that unit testing is the sole purpose of DI. Although that assumption is a different view than the late binding assumption, it, too, is a narrow view of a much broader vista.
If you thought that DI was only relevant for unit testing, unlearn this assumption. DI does much more than enable unit testing.
An Abstract Factory on steroids
Perhaps the most dangerous fallacy is that DI involves some sort of general-purpose Abstract Factory that we can use to create instances of the Dependencies that we need.
interface IUIControlFactory { IButton CreateButton(); ITextBox CreateTextBox(); }
Consider the following sentence: “collaborating classes … should rely on the infrastructure … to provide the necessary services.”
What were your initial thoughts? Did you think about the infrastructure as some sort of service you could query to get the Dependencies you need? If so, you aren’t alone. Many developers and architects think about DI as a service that can be used to locate other services; this is called a Service Locator, but it’s the exact opposite of DI.
It is often called an Abstract Factory on steroids, because compared to a normal Abstract Factory, the list of resolvable types is unspecified and possibly endless. DI typically has one method allowing the creation of all sorts of types, much like in the following listing:
interface IServiceLocator { object GetService(Type serviceType); }
If you thought of DI as a Service Locator-that is, a general-purpose Factory- this is something you need to unlearn. DI is the opposite of a Service Locator; it’s a way to structure code so that we never have to imperatively ask for Dependencies. Rather, we force consumers to supply them.
DI Containers
Closely associated with the previous misconception is the notion that DI requires a DI Container. If you held the previous, mistaken belief that DI involves a Service Locator, then it’s easy to conclude that a DI Container can take on the responsibility of the Service Locator. This might be the case, but it’s not at all how we should use a DI Container.
A DI Container is an optional library that can make it easier for us to compose components when we wire up an application, but it’s in no way required. When we compose applications without a DI Container we call it Pure DI; it might take a little more work, but other than that we don’t have to compromise on any DI principles.
note: If you thought that DI requires a DI Container, this is another notion you need to unlearn. DI is a set of principles and patterns, and a DI Container is a useful, but optional tool.
You may think that, although we’ve exposed four myths about DI, we have yet to make a compelling case against any of them. That’s true. In our experience, unlearning is vital because people tend to try to retrofit what we tell them about DI and align it with what they think they already know. When this happens, it takes a lot of time before it finally dawns on them that some of their most basic premises are wrong. We want to spare you that experience. So, if you can, try to read this book as though you know nothing about DI.
Accordingly, let’s assume that you don’t know anything about DI or its purpose and begin by reviewing what DI does.
Understanding the purpose of DI
Like we mentioned before, DI isn’t an end-goal-it’s a means to an end. DI enables loose coupling, and loose coupling makes code more maintainable. That’s quite a claim, and although we could refer you to well-established authorities like the Gang of Four for details, we find it only fair to explain why this is true.
To get this message across, we will compare software design and several software design patterns with electrical wiring in the physical world. We have found this to be a very powerful analogy and have even used it to explain software design to non-technical people as well.
The five design patterns we use in the analogy were chosen because they are very common in relationship with DI. Don’t worry if you’re not that familiar with them.
Software development is still a rather new profession, so in many ways we’re still figuring out how to implement good architecture. However, individuals with expertise in more traditional professions (such as construction) figured it out a long time ago.
When it comes to the construction industry, in particular, the inclusion of specialist software that can help to drive their processes forward will do wonders for most of the projects that they have to complete. Luckily for them, companies like RIB Software, who are based in Stuttgart (read – https://www.businessinsider.nl/dit-zijn-de-10-snelstgroeiende-bedrijven-van-duitsland-die-niemand-kent-524227/ for more information) specialize in cost analysis software and the calculation of supporting structures, so this can be of benefit to a lot of people who work in this industry. This just emphasizes the need for further software development.
Checking into a cheap hotel
If you’re staying at a cheap hotel, you might encounter a sight like the one in Figure 3. Here, the hotel has kindly provided a hair dryer for your convenience, but apparently they don’t trust you to leave the hair dryer for the next guest: the appliance is directly attached to the wall outlet. Although the cord is long enough to give you a certain degree of movement, you can’t take the dryer with you. You’ll find this is the case with several hotel appliances from mini-fridges to TVs. Apparently, the hotel management has decided that the cost of replacing stolen appliances is high enough to justify what is otherwise an obviously inferior implementation.
Figure 3. In a cheap hotel room, you might find the hair dryer wired directly into the wall outlet. This is equivalent to using the common practice of writing tightly coupled code.
What happens when the hair dryer or any other appliance in the room happen to stop working? The hotel has to call in a skilled professional like those you can find on http:www.chadsappliancerepair.com who can deal with the issue. To fix any appliance wired this way, they will have to cut the power to the room, rendering it temporarily useless. I remember my friend once visited a hotel and the microwave stopped working. The hotel manager found an electrician on a website like https://saltle.com/ and they were there within the hour. If you’re staying in a busy area, you might not be as lucky. If the issue is in the connection itself, the technician will use special tools to painstakingly disconnect the hairdryer and replace it with a new one. Sometimes, issues like this actually turn out to be switchboard related, meaning the hotel might need an electrical switchboard upgrade to better the power supply going to the rooms. If you’re lucky, the technician will remember to turn the power to the room back on and go back to test whether the new hair dryer works… if you’re lucky.
Does this procedure sound at all familiar?
This is how you would approach working with tightly coupled code. In this scenario, the hair dryer is tightly coupled to the wall and you can’t easily modify one without impacting the other.
Comparing electrical wiring to design patterns
Usually, we don’t wire electrical appliances together by attaching the cable directly to the wall. Instead, as in Figure 4, we use plugs and sockets. A socket defines a shape that the plug must match.
Figure 4. Through the use of sockets and plugs, a hair dryer can be loosely coupled to the wall outlet.
In an analogy to software design, the socket is an interface and the plug with its appliance the implementation. This means that the room (our application) has one or (hopefully) more sockets, and the user of the room (the developer) can plug in appliances as he or she pleases.
In contrast to the hardwired hair dryer, plugs and sockets define a loosely coupled model for connecting electrical appliances. As long as the plug (the implementation) fits into the socket (implements the interface) and it can handle the amount of volts and hertz (obeys the contract), we can combine appliances in a variety of ways. What’s particularly interesting is that many of these common combinations can be compared to well-known software design principles and patterns.
First, we’re no longer constrained to hair dryers. If you’re an average reader, we would guess that you need power for a computer much more than you do for a hair dryer. That’s not a problem: we unplug the hair dryer and plug a computer into the same socket, as shown in Figure 5.
Figure 5. Using sockets and plugs, we can replace the original hair dryer from Figure 4 with a computer. This corresponds to the Liskov Substitution Principle.
It’s amazing that the concept of a socket predates computers by decades, and yet it provides an essential service to computers, too. The original designers of sockets couldn’t possibly have foreseen personal computers, but because the design is so versatile, needs that were originally unanticipated can be met. The ability to replace one end without changing the other is similar to a central software design principle called the Liskov Substitution Principle. This principle states that we should be able to replace one implementation of an interface with another without breaking either client or implementation.
When it comes to DI, the Liskov Substitution Principle is one of the most important software design principles. It’s this principle that enables us to address requirements that occur in the future, even if we can’t foresee them today. We can unplug the computer if we don’t need to use it at the moment. Even though nothing is plugged in, the room doesn’t explode. If we unplug the computer from the wall, neither the wall outlet nor the computer breaks down. With software, however, a client often expects a service to be available. If the service was removed, we get a NullReferenceException
. To deal with this type of situation, we can create an implementation of an interface that does “nothing.” This is a design pattern known as Null Object [3], and it corresponds to having a children’s safety outlet plug, i.e. a plug without a wire or appliance that still fits into the socket. Because we’re using loose coupling, we can replace a real implementation with something that does nothing without causing trouble. This is illustrated in Figure 6.
Figure 6. Unplugging the computer causes neither room nor computer to explode when replaced with a children’s safety outlet plug. This can be roughly likened to the Null Object pattern.
There are many other things we can do. If we live in a neighborhood with intermittent power failures, we may wish to keep the computer running by plugging in into an Uninterrupted Power Supply (UPS), as shown in Figure 7. We can connect the UPS to the wall outlet and the computer to the UPS.
Figure 7. An Uninterrupted Power Supply can be introduced to keep the computer running in case of power failures. This corresponds to the Decorator design pattern.
The computer and the UPS serve separate purposes. Each has a Single Responsibility that doesn’t infringe on the other appliance. The UPS and computer are likely to be produced by two different manufacturers, bought at different times, and plugged in at different times. As we saw in figure 5, we can run the computer without a UPS, but we could also conceivably use the hair dryer during blackouts by plugging it into the UPS.
In software design, this way of Intercepting one implementation with another implementation of the same interface is known as the Decorator [4] design pattern. It gives us the ability to incrementally introduce new features and Cross-Cutting Concerns without having to rewrite or change a lot of our existing code.
Another way to add new functionality to an existing code base is to compose an existing implementation of an interface with a new implementation. When we aggregate several implementations into one, we use the Composite [5] design pattern. Figure 8 illustrates how this corresponds to plugging diverse appliances into a power strip.
Figure 8. A power strip makes it possible to plug several appliances into a single wall outlet. This corresponds to the Composite design pattern.
The power strip has a single plug that we can insert into a single socket, while the power strip itself provides several sockets for a variety of appliances. This enables us to add and remove the hair dryer while the computer is running. In the same way, the Composite pattern makes it easy to add or remove functionality by modifying the set of composed interface implementations.
Here’s a final example. We sometimes find ourselves in situations where a plug doesn’t fit into a particular socket. If you’ve traveled to another country, you’ve likely noticed that sockets differ across the world. If you bring something like the camera in Figure 9 along when traveling, you need an adapter to charge it. Appropriately, there’s a design pattern with the same name.
Figure 9. When traveling, we often need to use an adapter to plug an appliance into a foreign socket (for example, to recharge a camera). This corresponds to the Adapter design pattern.
The Adapter [6] design pattern works like its physical namesake. It can be used to match two related, yet separate, interfaces to each other. This is particularly useful when you have an existing third-party API that you wish to expose as an instance of an interface your application consumes.
What’s amazing about the socket and plug model is that, over decades, it’s proven to be an easy and versatile model. Once the infrastructure is in place, it can be used by anyone and adapted to changing needs and unpredicted requirements. What’s even more interesting is that, when we relate this model to software development, all the building blocks are already in place in the form of design principles and patterns.
The advantage of loose coupling is the same in software design as it is in our physical socket and plug model: once the infrastructure is in place, it can be used by anyone and adapted to changing needs and unpredicted requirements, without having to make large changes to the application’s code base and its infrastructure. This means that ideally, a new requirement should only require the addition of a new class, with no changes to other already existing classes of the system.
This concept of being able to extend the application without modifying existing code is called the Open/Closed Principle. It is impossible to get to a situation where 100% of your code will always be open for extensibility, but closed for modification. Still, with loose coupling we get closer, and it gets easier to add new features and requirements to our system. The ability to add new features without touching existing parts of the system means that our problems get isolated. This leads to code that is easier to understand and test. In other words, we’re managing the complexity of our system. That’s what loose coupling can help us with, and that’s why loose coupling can make a code base much more maintainable.
The easy part of loose coupling is programming to an interface instead of an implementation. The question is: where do the instances come from?
You can’t create a new instance of an interface the same way that you create a new instance of a concrete type. Code like this doesn’t compile:
IMessageWriter writer = 1 new IMessageWriter(); 2
1 Program to an interface
2 Does not compile
An interface has no constructor, so this isn’t possible. The writer
instance must be created using a different mechanism. DI solves this problem.
With this outline of the purpose of DI, we think you’re ready for an example.
If you want to learn more about the book, check it out on liveBook here.
You can also see other articles on common DI-related topics:
Understanding Property Injection
Understanding Method Injection
Understanding the Composition Root
Understanding Constructor Injection
Abuse of Abstract Factories
The Service Locator Anti-Pattern
The Ambient Context Anti-Pattern
The Transient Lifestyle
The Scoped Lifestyle
The Singleton Lifestyle
[1] Erich Gamma et al., Design Patterns: Elements of Reusable Object-Oriented Software (New York, Addison-Wesley,
1994), 18.
[2] Gamma, Design Patterns, 87.
[3] Robert C. Martin et al., Pattern Languages of Program Design 3 (New York, Addison-Wesley, 1998), 5.
[4] Gamma, Design Patterns, 175.
[5] Gamma, Design Patterns, 163.
[6] Gamma, Design Patterns, 139.