'. '

Injectable Singleton

From APIDesign

(Difference between revisions)
Jump to: navigation, search
(Testability)
(Injectable Meta-Singleton)
Line 78: Line 78:
* Lookup is meta!
* Lookup is meta!
* Show [[dependency injection]] cooperation? Anybody wants to see that?
* Show [[dependency injection]] cooperation? Anybody wants to see that?
 +
 +
== Polemic ==
 +
 +
TBD:
 +
* is it really singleton?
 +
* [[Separate APIs for Clients and Providers]]

Revision as of 04:14, 28 January 2010

Singletons are sometimes dishonest as a design anti-pattern. Right, one may use them improperly, but with guidance of proper methodology there is nothing wrong on singletons. This page provides step by step cook book for usage of so called Injected Singletons.

Contents

What this is not

We are going to introduce an enhanced version of the singleton pattern that supports testability, provides smooth Convention over Configuration and Component Injection. It can be seen as nice alternative companion to application context used by dependency injection.

However when talking about singleton pattern, don't imagine Java's SecurityManager or URLStreamHandlerFactory. Those are not Injectable Singletons, they require non-trivial amount of initialization code and basically let your application knowledge leak out through out the system.

Defining the Singleton

Imagine you want to create a singleton service to display questions to the user. This would be your API:

Code from DialogDisplayer.java:
See the whole file.

public abstract class DialogDisplayer {
    protected DialogDisplayer() {
    }
 
    /** Ask user a question.
     *
     * @param query the text of the question
     * @return true if user confirmed or false if declined
     */
    public abstract boolean yesOrNo(String query);
 
    public static DialogDisplayer getDefault() {
        return Impl.DEFAULT;
    }
}
 

Slightly surprising thing is that the class is abstract (e.g. it contains no implementation) and to allow extensibility it provides protected constructor. This is not what traditional singletons usually do, but remember this is an Injectable Singleton!

As the important motto of Injectable Singletons is Convention over Configuration each such singleton needs to define default (even dummy) implementation. Let's do it like this:

Code from DialogDisplayer.java:
See the whole file.

private static final class Impl extends DialogDisplayer {
    private static final DialogDisplayer DEFAULT = initialize();
 
    @Override
    public boolean yesOrNo(String query) {
        System.err.printf("Saying no to '%s'\n", query);
        return false;
    }
}
 

In this case, the implementation of the yesOrNo method is really trivial and not very useful, but often there are cases when the default behavior can be made acceptable for many purposes.

Now the time as come to write proper initialization code (that will support extensibility). We can do it either in Java6 standard way via ServiceLoader:

Code from DialogDisplayer.java:
See the whole file.

private static DialogDisplayer initializeServiceLoader() {
    Iterator<DialogDisplayer> it = null;
    it = ServiceLoader.load(DialogDisplayer.class).iterator();
    return it != null && it.hasNext() ? it.next() : new Impl();
}
 

Or using small Lookup library (we'll see later what benefit it provides):

Code from DialogDisplayer.java:
See the whole file.

private static DialogDisplayer initializeLookup() {
    final Lookup lkp = Lookup.getDefault();
    DialogDisplayer def = lkp.lookup(DialogDisplayer.class);
    return def != null ? def : new Impl();
}
 

And that is all. Our first, Injectable Singleton is ready for being used.

Usage

To use a singleton, it is enough to import its API class and call its methods. Because singletons are inherently initialized - e.g. as soon as one calls their static getter, one gets initialized instance - there is no need for any special configuration in the client code. Just:

Code from Main.java:
See the whole file.

import org.apidesign.singletons.api.DialogDisplayer;
 
public class Main {
    public static void main(String[] args) {
        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!"
            );
        }
    }
}
 

This makes the whole use as simple as possible by following half of the Convention over Configuration motto: If you are OK with default implementation, just use it. As soon as you link with the API class, you can be sure that you'll get reasonable implementation. No other configuration is needed. This is the output of such sample execution:

$ java -jar DisplayerMain.jar -cp Displayer.jar
Saying no to 'Do you like singletons?'
Visit http://singletons.apidesign.org to change your mind!

Configure

Of course, using the dummy implementation is not always optimal. As our systems get more and more assembled at deploy time there especially needs to be a way to support easy configuration during application deployment. This is inherently supported due to adherence to Java Standard Extension Mechanism. Any time later, in any additional project that produces completely independent JAR file one can compile following piece of code:

Code from SwingDialogDisplayer.java:
See the whole file.

@ServiceProvider(service=DialogDisplayer.class)
public final class SwingDialogDisplayer extends DialogDisplayer {
    @Override
    public boolean yesOrNo(String query) {
        final int res = JOptionPane.showConfirmDialog(null, query);
        return res == JOptionPane.OK_OPTION;
    }
}
 

The JAR containing this class (and the generated META-INF/services/ entry, as generated by processing @ServiceProvider annotation via compile time AnnotationProcessor or alternatively created manually) can be put onto the application classpath. As soon as you do

$ java -jar DisplayerMain.jar -cp Displayer.jar:DefaultDisplayerImpl.jar

your system will get properly configured and will use the injected enhanced singleton:

Image:Singleton.png

Don't waste time writing configuration files! All you need to do to configure Injectable Singletons is to include correct JARs on application classpath. Follow the second part of the Convention over Configuration motto. Let application assemblers dissatisfied with default, drop-in their implementations to classpath!

Testability

Of course, these days one just has to write code that is inherently testable. One of the critiques of singleton pattern was that it does not allow this. That it is not possible to mock in different singleton behavior during test execution. Injectable Singletons are here to allow you to do exactly this:

Code from MainTest.java:
See the whole file.

public class MainTest {
    @BeforeClass
    public static void setUpClass() throws Exception {
        MockServices.setServices(MockDialogDisplayer.class);
    }
 
    @Test
    public void testMainAsksAQuestion() {
        assertNull(
            "No question asked yet", MockDialogDisplayer.askedQuery
        );
        Main.main(new String[0]);
        assertNotNull(
            "main code asked our Mock displayer",
            MockDialogDisplayer.askedQuery
        );
    }
 
    public static final class MockDialogDisplayer extends DialogDisplayer {
        static String askedQuery;
 
        @Override
        public boolean yesOrNo(String query) {
            askedQuery = query;
            return false;
        }
    }
}
 

The first thing the test does, is to inject its own, mock implementation of singleton via utility class MockServices. Then the whole test execution is using this instance, which is obviously enhanced to allow easy testability.

I shall however mention an important restriction. Because Injected Singletons support class-level co-existence, each set of tests that get linked together share the same singletons. This may not be always appropriate - in such case split your test into two classes and make sure they get executed in a separate VM.

Injectable Meta-Singleton

TBD:

Polemic

TBD:

Personal tools
buy