Dependency injection can throw you into analysis paralysis. Here are some rules of thumb.
Either a dependency is assigned for the lifetime of an object or it is passed in as the parameter of a method.
The mechanism that performs dependency injection is itself a dependency. Limit its scope to the edges of a use case.
I’ve worked on numerous distributed systems. Two of the abstractions used in Java distributred systems are Remote Method Invocation (RMI) Java Message Service (JMS). Both have their uses, and inversion of control should play nicely with either.
In both cases, the edges of the use case are when remote requests come across the wire. JMS vary naturally provides a tie in with an inversion of control container in the mapping of a newly received message to the code that is supposed to handle it:
HandlerMessageHandler = context.get(message.getType());
In an RMI system, each remote may provide a tie in to the context. If there are no resources that are allocated solely for the purpose of processing the request, there is no need to create a new context. On the other hand, Remote objects tend to be long lived, and often need access to resources only for a short time. Thus the remote method will often need to create a thread scoped context for object resolution. While this context can be provided by a proxy, this leads to a really awkward setup where the Remote object knows nothing about the context. If objects downstream from the remote object need access to the context, they haveto get it by Magic, and you end up with the same type of nasty code that you get in most JEE applications: hard coded factories, JNDI lookups and the like. If the client calls:
remoteObject->munge(myMessage);
The remote object has code like:
void munge(MyMessage){
Resource r = ResourceFactory.getInstance().create();
}
One alternative is to have the dependencies passed in to the object that implements the remote interface. The awkwardness now is that the caller and implementer have two different contracts.
The client calls
remoteObject->munge(myMessage);
But the remote object implements
void munge(MyMessage myMessage, Resource resource);
Injection at the edges would lookl ike this:
void munge(MyMessage myMessage){
munge( myMessage, new Context<Resource>().get( ));
}
Here the remote object has transparency into the creation of the object. In order to write a unit test, we can call on the two parameter version of munge with a mock Resource object. The main difference between the Context version and the Factory version is the unification of the object creation mechanism in the Context version.
As an aside: If You need to have a dependency for a really short point in a time on the interior of a use case, you can use a lazy-load proxy. I don’t advise this, but it is an option. The first problem with this approach is that it doesn’t provide a clean way to clean up once the object is no longer required. The second is that object creation can fail, and the calling object may not cleanly handle that.