Working with Injectable Singletons-Collection of common programming errors
The concept described in that article is an Ambient Context that uses a Service Locator in the background.
Because of the use of a static property and the use of the Service Locator, this pattern is very inconvenient for unit testing. To be able to run a test that verifies code that uses this singleton, you need to set up a valid Service Locator and configure it with the singleton (probably a mock instance) that you care about using testing.
Even the example given by the article already suffers from these problems, since the “Do you like singletons?” code, is hard to test:
if (DialogDisplayer.getDefault().yesOrNo(
"Do you like singletons?"
)) {
System.err.println("OK, thank you!");
} else {
System.err.println(
"Visit http://singletons.apidesign.org to"
+ " change your mind!"
);
}
A better alternative is to use constructor injection to inject that singleton (please excuse my French, but I’m not a native Java speaker):
public class AskTheUserController
{
private DialogDisplayer dialogDisplayer;
private MessageDisplayer messageDisplayer;
public AskTheUserController(DialogDisplayer dialogDisplayer,
MessageDisplayer messageDisplayer)
{
this.dialogDisplayer = dialogDisplayer;
this.messageDisplayer = messageDisplayer;
}
public void AskTheUser()
{
if (this.dialogDisplayer.yesOrNo(
"Do you like singletons?"
)) {
this.messageDisplayer.display("OK, thank you!");
} else {
this.messageDisplayer.display(
"Visit http://singletons.apidesign.org to"
+ " change your mind!"
);
}
}
}
There was another ‘hidden’ dependency in that code: System.err.println. It got abstracted using a MessageDisplayer interface. This code has a few clear advantages:
- By injecting both dependencies, the consumer doesn’t even need to know that those dependencies are singletons.
- The code clearly communicates the dependencies it takes.
- The code can easily be tested using mock objects.
- The test code doesn’t need to configure a service locator.
Your tests might look like this:
@Test
public void AskTheUser_WhenUserSaysYes_WeThankHim()
{
// Arrange
bool answer = true;
MockMessageDisplayer message = new MockMessageDisplayer();
MockDialogDisplayer dialog = new MockDialogDisplayer(answer);
AskTheUserController controller =
new AskTheUserController(dialog, message);
// Act
controller.AskTheUser();
// Assert
Assert.AreEqual("OK, thank you!", message.displayedMessage);
}
@Test
public void AskTheUser_WhenUserSaysNo_WeLetHimChangeHisMind()
{
// Arrange
bool answer = true;
MockMessageDisplayer message = new MockMessageDisplayer();
MockDialogDisplayer dialog = new MockDialogDisplayer(answer);
AskTheUserController controller =
new AskTheUserController(dialog, message);
// Act
controller.AskTheUser();
// Assert
Assert.IsTrue(
message.displayedMessage.contains("change your mind"));
}
Your test code will never be as intend revealing as the code above when you’re using the ‘injectable singleton’ pattern as shown in the article.