Objects are composed of objects. The pen in your hand is composed of a nib, a cartridge, and a plastic sheath. The cartridge contains ink, some kind of valve and so forth. The word "pen" is an abstraction that allows us to communicate at a higher-level and ignore all that detail.
However, when you use a dependency injection (DI) framework, like Spring, to wire objects together, everything appears to be at the same level of abstraction.
Abstraction is the key to dealing with complexity
Without being able to distinguish higher-level abstractions it's easy to become mired in low-level implementation detail. The complexity makes it harder to reason about the application and hard to maintain a clean separation of concerns.
Find clusters that can be abstracted
To work at higher levels of abstraction, we need to encapsulate a graph of lower-level objects inside a simpler façade. The GOOS authors call this "the rule of composite simpler than the sum of its parts" (p.53).
Inner objects are not peers
The encapsulated objects are inside the higher-level object. They are not external collaborators of the object. The inner objects are an implementation detail.
To construct our objects we have two basic choices: either (1) we pass inner objects in through the constructor/setters (à la Spring) or (2) we create the inner objects inside the outer object.
Injecting inner-objects breaks encapsulation
If we pass objects in, we make it hard to distinguish internals from peers and we break encapsulation because we are forced to know how the object is implemented when we construct it.
Use "new" instead
The simple alternative is to use the
new operator to create the internal objects and wire them together in the outer object's constructor. If we do this consistently through our application, we don't actually need a DI framework. Objects themselves are quite capable of performing the role that a DI container would perform.
If necessary, mix the two approaches
There are occasions when having separated configuration can be useful - e.g. for supporting plugins. There is nothing stopping us from adopting a hybrid of the two approaches. We can use a DI container for those specific objects that need it.
And vice versa: if we're already using Spring, there's nothing to stop us pulling out clusters of objects from the Spring configuration and assembling them in code, one cluster at a time. It doesn't have to be an all-or-nothing transition. It can be done gently.