Tuesday, 19 June 2012

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.

11 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.

@Steve FreemanHmm... 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?

@VasilyThe 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.

New just passes off the responsibility of resolving dependencies to the classloader (i.e. encapsulation is a myth, deal with it). Most mortals would prefer configuring a DI container to rolling their own classloader.

@CurtainDogWhat I'm trying to say in this post is that encapsulation is useful for human comprehension. I agree that encapsulation can be broken if you want to.

When I'm talking about using "new", I'm not suggesting using a custom classloader to resolve dependencies. Not at all! Just use normal constructors with the dependencies explicitly stated and passed in when you do "new".

One more things - which DI containers resolve is that they "know" how to construct the objects. Because low level dependencies also can have their own dependencies and etc. So creating such objects with "new" keyword increase coupling between high level holder and low level component. Car - should have no knowledge about how to construct engine, but it can use it. Maybe here Car and Engine - are objects on same level of abstraction but I think it depends on situation.

What I'm trying to say is that it's perfectly fine to inject an Engine into a Car (through a constructor or a setter). It's also fine for the Car to construct its own engine.

But, the engine is an internal of the car and therefore is something we would like to hide from peer objects (such as a RaceTrack). We want the construction of the car to be done by the right object: not by a peer, and I would argue not by a DI framework either.

We don't want the RaceTrack to construct Cars and specify the engines. There's no need for the RaceTrack to have that kind of deep knowledge about how cars are constructed. So what we can do is have an outer object construct the cars. And then outer objects construct them etc.

We now have objects like RacingTeam and RaceMeeting that give us insight into the application structure at a higher level of abstraction. If we were doing this in Spring, we could wire the lower-level objects together without creating objects like RedBullRacingTeam, but I would argue that the domain model is poorer without them.