Where Have All the Singletons Gone?
Where Have All the Singletons Gone?
Join the DZone community and get the full member experience.Join For Free
Atomist automates your software deliver experience. It's how modern teams deliver modern software.
In Singletons are Pathological Liars we discussed the problems of having singletons in your code. Let’s build on that and answer the question “If I don’t have singletons how do I ensure there is only one instance of X and how do I get X to all of the places it is needed?”
An OO application is a graph of objects. There are three different kinds of graphs I think of when I design an application
- Collaborator Graph: T graph of objects that would be emitted if you serialized your application. This shows which objects are aware of which others. (through object’s fields)
- Construction Graph: This graph shows which object created which other ones.
- Call Graph: This graph shows . A stack-trace would be a single slice through this graph.
If the new operators are mixed with application logic (see: How to Think About the new Operator) then the Constructor Graph and the Collaborator Graph tend to be one and the same. However, in an application which uses Dependency Injection the two graphs are completely independent. Lets have a closer look at our CreditCardProcessor example. Suppose this is our collaborator graph which we need to execute a request.
The above shows the application collaborator graph. The letter (S/R) in the corner designates object lifetime; either Singleton or Request scope. Now, just to be clear, there is nothing wrong with having a single instance of a class. The problem arises only when the singleton is available through a global “instance” variable as in Singleton.getInstance().
The HTTP request would come to AuthenticatorPage which would collaborate with Authenticator to make sure the user is valid and forward a valid request onto ChargePage which would then try to load the user from UserRepository and create the credit card transaction which would be processed by CrediCardProcessor. This in turn would collaborate with OfflineQueue to get the work done.
Now, in order to have a testable codebase we have to make sure that we don’t mix the object construction with application logic. So all of the above objects should rarely call the new operator (value objects are OK). Instead each of the objects above would declare its collaborators in the constructor. AuthenticatorPage would ask for ChargePage and Authenticator. ChargePage would ask for CreditCardProcessor and UserRepository. And so on. We have moved the problem of construction elsewhere.
In our tests it is now easy to instantiate the graph of objects and substitute test-doubles for our collaborators. For example if we would like to test the AuthenticatorPage, we would instantiate a real AuthenticatorPage with mock ChargePage and mock Authenticator. We would than assert that a request which comes in causes appropriate calls on Authenticator and ChargePage only if authentication is successful. If the AuthenticatorPage were to get a reference to Authenticator from global state or by constructing it, we would not be able to replace the Authenticator with a test-double. (This is why it is so important not to mix object construction with application logic. In the unit-test what you instantiate is a sub-set of the whole application. Hence the instantiation logic has to be separate from application logic! Otherwise, it’s a non-starter.)
So now the problem is, how do we construct the graph of objects?
In short we move all of the new operators to a factory. We group all of the objects of similar lifetime into a single factory. In our case all of the singletons end up in ApplicationFactory and all of the Pages end up in RequestFactory. The main method of our application instantiates an ApplicationFactory. When we call build() the ApplicationFactory in turn instantiates its list of objects (Database, OfflineQueue, Authenticator, UserRepository, CreditCardProcessor and RequestFactory). Because each of the objects declares its dependency, the ApplicationFactory is forced to instantiate the objects in the right order. In our case it must instantiate the Database first and than pass the reference to UserRepository and OfflineQueue. (The code will simply not compile any other way.)
Notice that when we create a RequestFactory we must pass in references to the Authenticator, UserRepository and CreditCardProcessor. This is because when we call build() on RequestFactory it will try to instantiate AuthenticatorPage which needs the Authenticator. So we need to pass the Authenticator into the constructor of RequestFactory and so on.
At run-time an HTTP request comes in. The servlet has a reference to RequestFactory and calls build(). The servlet now has a reference to the AuthenticatorPage and it can dispatch the request for processing.
Important things to notice:
- Every object only has references to what it needs directly! No passing around of objects which are not directly needed by the code. There is no global state at all. Dependencies are obvious since each object only asks for what it needs.
- If an object needs a reference to a new dependency it simply declares it. This change only affects the corresponding factory, and as a result, it is very isolated.
- All of the new operators end up in the factories; application logic is devoid of new operators.
- You group all of the objects with the same lifetime into a single factory (If the factory gets too big you can break it up into more classes, but you can still think of it as a single factory)
- The problem of “how do I ensure that I only have one of something” is nicely sidestepped. You instantiate only a single ApplicationFactory in your main, and as a result, you only instantiate a single instance of all of your singletons.
Opinions expressed by DZone contributors are their own.