A class that exposes different methods depending o

2019-09-14 09:19发布

问题:

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.

回答1:

Yes, you can do that like this:

  1. Define an interface (perhaps internal to the module providing these services); I'll call it ThingyImplementation.

  2. Put the code using the optional library (Guava, etc.) in a distinct class that you don't directly reference in any other code. That class implements ThingyImplementation.

  3. At runtime, dynamically attempt to load the class using the optional librayr using Class.forName, and instantiate it via Class#newInstance, assigning the result to a variable declared of type ThingyImplementation. Use the resulting instance via the interface as part of providing the functionality of the .

Loading or instantiating the class in step 3 will throw an exception if the optional library isn't available on the classpath, which you can either propagate or wrap in your own exception.

Note that the outer class (UnifiedVerifier) cannot directly refer to types defined by the optional library, so you couldn't have MultimapVerifier verify(Multimap); in it. If you have to provide that, it would have to accept Object and then cast within the optional module implementation as necessary.



回答2:

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: https://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)

UPDATE: I have posted an updated question for Java 9.