From Dependency Injection Principles, Practices, and Patterns by Steven van Deursen and Mark Seemann
Save 37% on Dependency Injection Principles, Practices, and Patterns. Just enter code fccseemann into the discount code box at checkout at manning.com.
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).
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 , 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.
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.
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.
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.
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.
The Abstract Factory pattern provides a set of factory methods. An Abstract Factory is typically an Abstraction that contains multiple methods, where each method allows the creation of an object of a certain kind.
A typical use case for the Abstract Factory pattern is user interface toolkits or client applications which must be able to run on multiple platforms. To achieve a high degree of code reusability on all platforms we could define an
IUIControlFactory Abstraction that allows consumers the creation of certain kinds of controls, like text boxes and buttons:
For each operating system, we could have a different implementation of this
IUIControlFactory. In this case, there are only two Factory Methods, but depending on the application or toolkit, there could be many more. The important thing, however, is that an Abstract Factory specifies a predefined list of Factory Methods.
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:
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.
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.
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. Apparently, the hotel management has decided that the cost of replacing stolen hair dryers is high enough to justify what is otherwise an obviously inferior implementation.
What happens when the hair dryer stops working? The hotel has to call in a skilled professional who can deal with the issue. To fix the hardwired hair dryer, they will have to cut the power to the room, rendering it temporarily useless. Then, the technician will use special tools to painstakingly disconnect the hair dryer and replace it with a new one. 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.
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.
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 , 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.
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.
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 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 design pattern. Figure 8 illustrates how this corresponds to plugging diverse appliances into a power strip.
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.
The Adapter 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 = ❶
new IMessageWriter(); ❷
❶ Program to an interface
❷ 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.
About the authors:
Mark Seemann is a programmer, software architect, and speaker who has been working with software since 1995, including six years with Microsoft. Steven van Deursen is a seasoned .NET developer and architect, and the author and maintainer of the Simple Injector DI library.
Originally published at freecontent.manning.com.