1

I need to support two versions of a dependency, which have the same API but different package names.

How do I handle this without maintaining two versions of my code, with the only change being the import statement?

For local variables, I guess I could use reflection (ugly!), but I use the classes in question as method argument. If I don't want to pass around Object instances, what else can I do to abstract from the package name?

Is it maybe possible to apply a self-made interface - which is compatible to the API - to existing instances and pass them around as instance of this interface?

I am mostly actually using xtend for my code, if that changes the answer.

kutschkem
  • 7,826
  • 3
  • 21
  • 56
  • 1
    if it's merely import statement, you could perhaps look into using a preprocessor, that'd substitute the right package depending on some flag – Coderino Javarino Aug 19 '19 at 11:14
  • could you not import one package into the other as thus you were creating a new project? – OlaB Aug 19 '19 at 11:30
  • "I need to support two versions of a dependency, which have the same API but different package names." Seems like an XY problem. **Why**? – Michael Aug 19 '19 at 12:32
  • @Michael I am writing a code generator for multiple middlewares. For one of them, I need to support multiple API versions. This works fine for the first three versions but the fourth one updated a 3rd party dependency which changed the package names. So I can't just update the dependency, I have to keep support for the older version in place. Is it fair to just say business requirement? – kutschkem Aug 19 '19 at 12:44
  • 2
    @kutschkem Keep one code-base for the newest, which you compile and JAR as normal. Then recompile sources [with ByteBuddy hooking in to replace the types](https://stackoverflow.com/questions/39742821/how-do-you-change-imports-with-byte-buddy) with the old package names and JAR these with a qualifier. Have dependent projects choose the qualified JAR which is applicable to them. – Michael Aug 19 '19 at 12:54
  • If the API consists only of a few simple interfaces and the versions have identical method signatures, you could create classes for each interface pair that implements both versions. Since you are using Xtend, you might be able to use [@Delegate](https://www.eclipse.org/xtend/documentation/204_activeannotations.html#delegate-annotation) active annotation to avoid writing most boilerplate code for delegating to the right API version. – kapex Aug 25 '19 at 11:59

1 Answers1

0

Since you're using Xtend, here's a solution that makes use of Xtend's @Delegate annotation. There might be better solutions that aren't based on Xtend though and this will only work for simple APIs that only consist of interfaces with exactly the same method signatures.

So assuming you have interfaces with exactly the same method signatures in different packages, e.g. like this:

package vendor.api1

interface Greeter {
    def void sayHello(String name)
}
package vendor.api2

interface Greeter {
    def void sayHello(String name)
}

Then you can combine both into a single interface and only use only this combined interface in your code.

package example.api

interface Greeter extends vendor.api1.Greeter, vendor.api2.Greeter {
}

This is also possible in Java so far but you would have to write a lot boilerplate for each interface method to make it work. In Xtend you can use @Delegate instead to automatically generate everything without having to care how many methods the interface has or what they look like:

package example.internal

import example.api.Greeter 
import org.eclipse.xtend.lib.annotations.Delegate
import org.eclipse.xtend.lib.annotations.FinalFieldsConstructor

@FinalFieldsConstructor
class GreeterImpl implements Greeter {
    @Delegate val Api delegate
}

@FinalFieldsConstructor
class Greeter1Wrapper implements Greeter {
    @Delegate val vendor.api1.Greeter delegate
}

@FinalFieldsConstructor
class Greeter2Wrapper implements Greeter { 
    @Delegate val vendor.api2.Greeter delegate
}

Both Greeter1Wrapper and Greeter2Wrapper actually implement the interface of both packages here but since the signature is identical all methods are forwarded to the respective delegate instance. These wrappers are necessary because the delegate of GreeterImpl needs to implement the same interface as GreeterImpl (usually a single delegate would be enough if the packages were the same).

Now you can decide at run-time which version to use.

val vendor.api1.Greeter greeterApi1 = ... // get from vendor API
val vendor.api2.Greeter greeterApi2 = ... // get from vendor API

val apiWrapper = switch version {
    case 1: new Greeter1Wrapper(greeterApi1)
    case 2: new Greeter2Wrapper(greeterApi2)
}

val example.api.Greeter myGreeter = new GreeterImpl(apiWrapper)
myGreeter.sayHello("world")

This pattern can be repeated for all interfaces. You might be able to avoid even more boilerplate by implementing a custom active annotation processor that generates all of the required classes from a single annotation.

kapex
  • 28,903
  • 6
  • 107
  • 121