Chapter 1. Dependency injection: what’s all the hype?
This chapter covers:
- Seeing an object as a service
- Learning about building and assembling services
- Taking a tour of pre-existing solutions
- Investigating the Hollywood Principle
- Surveying available frameworks
“We all agree that your theory is crazy, but is it crazy enough?”
Niels Bohr
So you’re an expert on dependency injection (DI); you know it and use it every day. It’s like your morning commute—you sleepwalk through it, making all the right left turns (and the occasional wrong right turns before quickly correcting) until you’re comfortably sitting behind your desk at work. Or you’ve heard of DI and Inversion of Control (IoC) and read the occasional article, in which case this is your first commute to work on a new job and you’re waiting at the station, with a strong suspicion you are about to get on the wrong train and an even stronger suspicion you’re on the wrong platform.
Or you’re somewhere in between; you’re feeling your way through the idea, not yet fully convinced about DI, planning out that morning commute and looking for the best route to work, MapQuesting it. Or you have your own home-brew setup that works just fine, thank you very much. You’ve no need of a DI technology: You bike to work, get a lot of exercise on the way, and are carbon efficient.
Stop! Take a good, long breath. Dependency injection is the art of making work come home to you.
1.1. Every solution needs a problem
Most software today is written to automate some real-world process, whether it be writing a letter, purchasing the new album from your favorite band, or placing an order to sell some stock. In object-oriented programming (OOP), these are objects and their interactions are methods.
Objects represent their real-world counterparts. An Airplane represents a 747 and a Car represents a Toyota; a PurchaseOrder represents you buying this book; and so on.
Of particular interest is the interaction between objects: An airplane flies, while a car can be driven and a book can be opened and read. This is where the value of the automation is realized and where it is valuable in simplifying our lives.
Take the familiar activity of writing an email; you compose the message using an email application (like Mozilla Thunderbird or Gmail) and you send it across the internet to one or more recipients, as in figure 1.1. This entire activity can be modeled as the interaction of various objects.
Figure 1.1. Email is composed locally, delivered across an internet relay, and received by an inbox.
This highlights an important precept in this book: the idea of an object acting as a service. In the example, email acts as a message composition service, internet relays are delivery agents, and my correspondent’s inbox is a receiving service.
1.1.1. Seeing objects as services
The process of emailing a correspondent can be reduced to the composing, delivering, and receiving of email by each responsible object, namely the Emailer, InternetRelay, and RecipientInbox. Each object is a client of the next.
Emailer uses the InternetRelay as a service to send email, and in turn, the InternetRelay uses the RecipientInbox as a service to deliver sent mail.
The act of composing an email can be reduced to more granular tasks:
- Writing the message
- Checking spelling
- Looking up a recipient’s address
And so on. Each is a fairly specialized task and is modeled as a specific service. For example, “writing the message” falls into the domain of editing text, so choosing a TextEditor is appropriate. Modeling the TextEditor in this fashion has many advantages over extending Emailer to write text messages itself: We know exactly where to look if we want to find out what the logic looks like for editing text.
- Our Emailer is not cluttered with distracting code meant for text manipulation.
- We can reuse the TextEditor component in other scenarios (say, a calendar or note-taking application) without much additional coding.
- If someone else has written a general-purpose text-editing component, we can make use of it rather than writing one from scratch.
Similarly, “checking spelling” is done by a SpellChecker. If we wanted to check spelling in a different language, it would not be difficult to swap out the English SpellChecker in favor of a French one. Emailer itself would not need to worry about checking spelling—French, English, or otherwise.
So now we’ve seen the value of decomposing our services into objects. This principle is important because it highlights the relationship between one object and others it uses to perform a service: An object depends on its services to perform a function.
In our example, the Emailer depends on a SpellChecker, a TextEditor, and an AddressBook. This relationship is called a dependency. In other words, Emailer is a client of its dependencies.
Composition also applies transitively; an object may depend on other objects that themselves have dependencies, and so on. In our case, SpellChecker may depend on a Parser to recognize words and a Dictionary of valid words. Parser and Dictionary may themselves depend on other objects.
This composite system of dependencies is commonly called an object graph. This object graph, though composed of many dependencies, i...