Dependency injection is one of my favorite design patterns, not only for what it is but for how it has turned my own thinking on its head. It has helped me approach engineering problems from a new perspective, modeling problems as discrete systems integration scenarios rather than monolithic or ad hoc bandaging strategies.
As with most unfamiliar concepts, to understand dependency injection it helps to develop and analyze a familiar analogue. I have taken many a stab at coming up with these, and they have all failed in the same place: they lack a realistic comparison to object instantiation and lookup. I knew I had to find a common situation which involved creating something in the event of its absence, and I immediately thought of making a sandwich. Stick with me, this isn't as crazy as it sounds.
When making a sandwich, one typically begins by laying out all the components: bread, turkey, cheese, tomato, lettuce, mustard, etc. Each of these items was purchased at some point and made available for the task of constructing a sandwich. Let's imagine that for whatever reason, the bread was not available. Instead, the ingredients for plain wheat bread were on hand. Now before the sandwich can be made, the bread has to be made. That means finding the recipe, following its directions, and baking the bread. With this out of the way, a turkey sandwich on wheat follows.
Now let's imagine that instead of wheat bread, we want our sandwich on sourdough. Now we have to find a different recipe, follow different directions, and use different ingredients to bake our different bread. Clearly this is not an efficient way to go when we just want to enjoy our sandwich. Enter dependency injection.
With dependency injection, we the sandwich maker don't worry about how to acquire the parts of the sandwich. They might come from the store or be prepared from scratch, but this is done by someone else. All we care about is that we have them on hand, and that we follow one universal set of directions: put the turkey on the bread, put the cheese on the turkey, put the tomato, lettuce, and mustard on the cheese, and cover the whole thing with another slice of bread. If we want to use wheat bread, sourdough, or anything else it doesn't matter; the directions are still the same. We simply swap out one type for another.
The dependency injected sandwich is much like any other systems integration problem: prepare the components independently of their involvement with the process at hand. In an enterprise system, business objects don't instantiate other business objects, nor do they seek them from another source; they allow someone else (the container) to figure out how to instantiate or otherwise reference the dependency, and provide it directly for use.
This model allows new versions of components to be swapped for old ones, dummy modules to be put in place for testing, and so forth. It also extends well beyond the domain of software engineering, as the sandwich example shows.