Gili Gili - 4 years ago 87
Java Question

A class that exposes different methods depending on whether an optional module is loaded

I have a library consisting of multiple modules:


  • core

  • guava



The
core
module is mandatory, while
guava
is optional. There are other optional modules (this question represents a minimal testcase).

Each module exposes a set of methods that the user can invoke:

class CoreVerifier
{
MapVerifier verify(Map);
}

class GuavaVerifier
{
MultimapVerifier verify(Multimap);
}


What I want




  • Provide users a class that exports all the methods in a single place:

    class UnifiedVerifier
    {
    MapVerifier verify(Map);
    MultimapVerifier verify(Multimap);
    }

  • I want users to be able to use this class even if optional modules (e.g. guava) are missing at runtime. Meaning, the
    UnifiedVerifier
    is compiled with all libraries on the classpath but at runtime
    MultimapVerifier
    referenced by the second method is not present.

  • Users should be able to invoke the first method even if the second method (that depends on the guava module) is not available at runtime.

  • If users attempt to invoke the second method (that depends on the missing module) they should get a runtime exception.



What actually happens




  • If users invoke the first method from application code javac fails with:

    Application.java: cannot access MultimapVerifier
    class file for MultimapVerifier not found



Meaning, even though the first method is well-defined (the core module is available at compile-time) the compiler refuses to proceed because the second method (which they are not using) is referencing a class which is missing from the classpath.

Is there a way to achieve this sort of thing in Java?




Similar technique by assertj



assertj has a clever static-import mechanism whereby they declare a different
Assertions
class per module (core, guava) and Java's static import picks up the right method depending on the types you pass in. I am already using a similar mechanism for static methods, but now I want something similar for a case where I can't use static methods.

Answer Source

I figured out a way! You can use class shadowing to achieve the desired behavior.

  1. Declare two modules:
    • core
    • guava
  2. In the core library, declare an interface for the functionality exposed by each module. For example:
    • CoreVerifiers
    • GuavaVerifiers
  3. Declare GuavaVerifiers a second time in the guava module
  4. Each GuavaVerifiers interface should declare the methods implemented by that module. So, GuavaVerifiers in the core module should be empty while GuavaVerifiers in the guava module should contain the methods implemented by that module. (Meaning, if users only link against the core module they shouldn't be able to see any guava-related functionality)
  5. Implement the interfaces in each module. For example CoreVerifiersImpl should implement CoreVerifiers. GuavaVerifiersImpl in core module should be empty because GuavaVerifiers in core module is empty. On the other hand, GuavaVerifiersImpl in the guava module should implement the non-empty GuavaVerifiers interface.
  6. For each interface in step 2, declare an interface in the core module with default methods that delegate to an existing verifier. For example:

    public interface ForwardingCoreVerifiers
    {
      CoreVerifiers coreVerifiers();
    
      default CoreVerifiers method1()
      {
        coreVerifiers().method1();
      }
    }
    
  7. Finally, in the core library, declare an implementation that extends all of the forwarding interfaces:

    public final class Verifiers
      implements ForwardingCoreVerifiers, ForwardingGuavaVerifiers
    {
      public CoreVerifiers coreVerifiers()
      {
        return new CoreVerifiersImpl(...);
      }
      public CoreVerifiers guavaVerifiers()
      {
        return new GuavaVerifiersImpl(...);
      }
    }
    

Now here's the magic:

  • If users include the core module in their classpath then the compiler will prevent them from invoking any guava-related methods.
  • But, if users include the guava module in front of the core module on the classpath then suddenly users will see the guava-related methods (because the guava module GuavaVerifier interface will shadow the interface found in the core module).

Performance tips:

  • In Java 8, default methods don't perform as well as abstract classes: http://stackoverflow.com/a/30314501/14731
  • As such, I recommend using an abstract class for the core functionality (CoreVerifiers) and using default methods for the remaining modules (GuavaVerifiers)
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download