You will have an affinity to this article if your team is trying to move an existing application to Dependency Injection (DI) from a nest-of-singletons design, but it is proving difficult. Making a commit without entangling most of the source-base is desirable, but when you're retrofitting DI to a large application, it can be hard. This article also applies to the general case of moving legacy to DI, and it applies to Java, .Net, Python, Ruby etc even though it name-drops Google's Java DI container (Guice). It's just the title works with that more than PicoContainer, Spring etc.
Singletons happen
Singletons and static state have been accused of being problematic for many years.
There is no doubt that a DI application is a more testable and cleaner architecture than the sprawling singleton hair-ball. It is often the case, though, under time pressure, that simple Singletons make it easy to bridge large gaps between components. What starts with one or two singletons, can end up being tens or even hundreds, until the entire team declares that the code is unmaintainable. The boss is not about to commission a rewrite, and likes the promise of DI, so it seems pragmatic to refactor the current codebase towards it.
Once you have decided to change direction towards that DI nirvana, you have to know which path to take. Sure, you could stop feature development and bug fixing and just go for it, but that is not the right way. Instead, you wish to deliver new functionality at the same time, and experience tells us that a promise to put in Guice (or Spring etc) in a week or two ends up being many months later. Even if the DI work is performed on a branch that is divergent to the one where features and bug fixes are still being coded, there is a risk of merge-hell when the DI refactor is finished.
It is also not clear where to start. Do you put a DI container in main() with no registered components yet and commit that change first? Or do you start at the web-tier and move towards DI there? Also does the DI container get populated with the results of singleton lookups initially, with TODO comments promising a revisit later? Either of these can be messy and unsavory.
'Service Locator' as a stepping stone to Dependency Injection
Martin Fowler wrote the definitive article on Dependency Injection in 2003. The formal field for DI was young at the time, and Martin discussed Service Locator as a worthy alternative choice. Of course, it means different things to different people, but for now assume it means a single class (that is a singleton itself) with a method on it like so...
public Object getService(String serviceName) {
// etc
}
or
public T getService(ClassserviceType);
// etc
}
The idea is that in a boot-like place (the main method?) it is populated with appropriate instances for the service names, and that unit tests can program it with a mix of real and mock instances. Wherever there are singleton lookup in the legacy codebase, a small change is made to lookup the same component via the service locator instead. This is only good for 'application scoped' services/components. Modern web frameworks have session and request scoped components and handle the dependency injection for them as those scopes come into life. If you are heading towards DI, then it is likely that you do not yet have one of the modern web frameworks in the application, so live with what you have for now. The service locator needs to be populated in the primordial boot place...
public void setService(String serviceName, Object implementation) {
// etc
}
To make this safe, you need to have a mechanism to lock the service locator and make it read only. That lock() method would be called at the end of the main method before any start lifecycle were invoked and would very effectively prevent misuse.
Putting in your service locater is the just the start. What follows is a series of small commits where you are replacing one singleton's getInstance() method with getService() on the service locator. In effect these singletons have now become 'managed single instances'. You might find it a good time to interface/impl separate the component in question. One reason you might decide to do that is to facilitate mocking (google for EasyMock, JMock or Mockito) because you obviously want tos simultaneously increase the test coverage for the application.
When you have exhausted the list of Singletons to eliminate, you can revisit the code fragments that use the service locator. It might be that a getService invocation is done wherever the component/service was needed in a class. It may even be done multiple times in the same class (garbage collected each time when de-scoped). In that case, doing the getService invocation once in a constructor and storing the result as a member variable would be smart.
In the case of our stylized service locator components diagram (above), California (a component responsible for agriculture and high-tech) needs Nevada to provide gaming machine functionality. Oregon (Hazelnuts) and Arizona (more high tech). Others are not shown, but you get the picture.
As mentioned, it is desirable to do the changes in a series of small commits. Ones that will easy for others to merge in. They can easily be submitted simultaneously to the team's roll out of improved functionality and bug fixes. There should be low risk of roll-back. Even lower risk of roll-back if you are taking the opportunity to add small unit tests to these newly separated components at the same time.
Least Depending, Most Dependent first
The first component to move towards the service locator design, and away from its singleton origins, is the one that depends on no other singletons, yet may be depended by other singletons.
It's the lowest hanging fruit - the least depending and most depended on.
It is also going to be the one that is easiest to get high coverage for with small unit tests. Perhaps this means some work with your favorite mocking library. As you process one and commit, another will qualify as "least depending, most dependent".
Guice after a short delay
Now that you have an application that is comprised of many components accessed via a single service locator, it is time to put in Guice (or your preferred DI container) and start moving components out of the Service Locator and into Guice's modules (substitute the setup terms of your preferred container).
The most methodical way of doing this is to find one of the service locator lookups in a constructor and push it to the class instantiating it. Change it to a constructor argument at the same time, and make the caller have a member variable for that same dependency. If you keep pushing them up, sooner or later your going to get them to the main method. At that time they can safely become Guice managed.
When everything is managed by the DI container, the service locator can be thanked for its good work and deleted.
Some side effects are going to be long argument lists for some of the classes/components. These are likely to be clues that the design for the application could do with some work. Making a facade at that point can often be the right thing.
Many companies have had some success with this methodical approach. It works for situations where there are hundred of components with which started out as a nest of singletons, and are now lightweight Dependency Injection. It works too if EJB 3.0 is your destination. It is also the experience that roll-out can happen concurrently with normal coding, with no code-freeze to facilitate merges at all.
Footnote
Dependency Injection is just one part of Inversion of Control (now ten years old as a pattern). The other two aspects are configuration and lifecycle. The implication is that classes should be given their configuration (more injection) and lifecycle state-changes similarly controlled from outside. They should not get their own config or spawn threads or listen on sockets in their constructors or worse still static initializers.
About the Author
Paul Hammant has been promoting Inversion of Control since 2000, originally on Apache's Avalon framework then pioneering Costructor Injection with PicoContainer in 2003. He works for ThoughtWorks in San Francisco.