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.
7 comments:
I think, you misunderstand GOOS a bit. IMHO, "Internals" should be physically hidden inside façade classes (private nested classes in C#, for example). But another thing is really important: if you compose a large monolithic object from a bunch of smaller ones, then how on Earth you are going to test the former? Yes, you can test it as a whole on the very bottom layer of abstraction, when you are composing a relatively simple object from almost primitive ones. But you really shouldn't do such a thing on higher layers, because doing so you abolish Dependency Inject entirely.
I would disagree that the code for internal objects needs to be physically inaccessible from outside. Not least because that would make them impossible to unit test.
For the container, I tend to test the main paths through at that level, using lower-level tests to flush out some of the detail. As we work our way up the stack, higher-level objects will be made up of internal supporting objects and some passed-in collaborators.
Again, don't forget that containership is what the caller sees, not necessarily how the object is constructed.
This is a problem I really recognise. In a recent project, I have been trying to provide a set of components as reusable code libraries. I wanted to avoid mandating a DI framework for users of the component, and avoid the complexity of having Spring as a code dependency.
Our solution was to use Guice for lower-level DI, giving users the freedom to choose a DI framework at the higher level of abstraction (where Spring was the usual choice here). Guice is sufficiently lightweight that we felt more comfortable having it as a code dependency. It is also feels more 'natural' as Guice is predominantly used programmatically (as opposed to using external XML config), keeping the solution entirely in Java code.
This only gives us two levels of abstraction, but it does seem to have solved most of our problems.
I found using the 'new' keyword for DI creates a great deal of boilerplate code (unnecessary wiring and constructor code). With the development approaches I am familiar with (TDD) you typically end up with a huge number of classes that act as Singletons within the scope of the component. Wiring these together using 'new' at the top layer of abstraction can get messy.
@Vasily, dependency injection is not abolished. The "large monolithic object" (as you call it) is simply an object at a higher-level of abstraction. It will still have peers that need to be injected, but the dependencies are at a higher-level of abstraction too.
As Steve says, you can unit test the internals. They are just classes and don't have to be hidden from tests.
@Dave
Spring can be configured from Java as well as XML. A big advantage of Spring's Java Config over Guice is that you don't have to pollute your objects with annotations.
The point I'm trying to make is that generally there's no need to use a DI framework at all. It's not a criticism of Spring but DI containers in general.
Why was there so much boiler-plate code when you used "new"? Could that possible be an indication of a design problem?
@Steve Freeman
Hmm... I never considered the possibility of creating some of peers inside an object. After reading GOOS (it's excellent, btw) I concluded that 'internals' must be simple objects, not requiring any testing (the example in the book shows only one such a class and it's really simple). If I understand you correctly, it's ok to 'new' some not trivial aggregated objects in a larger object's constructor? But I don't understand how in this case to *unit* test the larger object?
@Vasily
The internals of a higher-level object are graphs of objects - i.e. several lower-level objects created and wired up by the container, along with references to peer objects passed in via its constructor or via setters.
The objects inside an object are generally at a lower-level of abstraction than the container. (Their domain is "how" to implement the "what" of the higher-level container's interface).
In terms of testing the higher-level objects, the higher-level objects can implement interfaces just like any other object, so you can construct higher-level objects with mocked peers to test them, just like you would any other object. The behaviour of a higher-level object will be more complex, however, so you will want to unit test its inner objects individually as well.
Post a Comment