I am trying to encapsulate a third-party library by writing a wrapper over it and using only required APIs as per my need.
To achieve this, I have created this wrapper project as a standalone project with all the dependencies of third-party library.
The third-party library may be changed with a better one in future so, a great level of decoupling is required.
While doing so, I came across some Service Provider Interfaces by the third-party library which are loaded using the a java ServiceLoader somewhere in the library.
I want users of my wrapper-project to be totally unaware of the third-party library that is being used.
To achieve so, I must write my wrapper-project such that, no dependency of third-party should be required by the users.
But SPIs often need their configuration to be done in META-INF/services/{SPIName} file of the project which will eventually result in leakage of third-party knowledge.
How can I encapsulate these third part SPIs so that the end user is totally unaware of the third-party library?
Is there a technical term for such cases? or Is there an existing solution or design pattern for this?
I have tried a couple of things after exhausting my online search
Third-Party SPI
package com.thirdParty.library;
public interface IDataCollector {
String getStudentName();
Integer getMarks();
}
Some class where all the service providers for this SPI are loaded. Please note that in actual third party it is as complex as complex can be.
public class RankCalculator {
private static final Map<Integer, String> ranks = new TreeMap<>();
public RankCalculator () {
ServiceLoader<IDataCollector> dataCollectorLoader = ServiceLoader.load(IDataCollector.class);
for (IDataCollector dataCollector: dataCollectorLoader) {
ranks.put(dataCollector.getMarks(), dataCollector.getStudentName());
}
}
public Integer getRank (String name) {
Integer position = ranks.size();
for (Map.Entry<Integer, String> entry: ranks.entrySet()) {
if (entry.getValue().equals(name)) {
return position;
}
position --;
}
return -1;
}
}
Wrapper-Project: I planned to write an interface in my wrapper of IdataCollector
and then implementations of that interface and I could also register that in META-INF/services. Furthermore, I thought I would expose IWrapDataCollector with the end-users and they'll register their META-INF/services with this interface. it goes like this:
package com.myWrapper.test;
import com.thirdParty.library.IDataCollector;
public interface IWrapDataCollector extends IDataCollector {
default Integer getMarksWrapper() {
return null;
}
default String getStudentsNameWrapper() {
return null;
}
@Override
default String getStudentName() {
return getStudentsNameWrapper();
}
@Override
default Integer getMarks() {
return getMarksWrapper();
}
}
META-INF/services/com.thirdParty.library.IDataCollector
with entry as com.myWrapper.test.IWrapDataCollector
but I soon realized that ServiceLoader is able to load only concrete classes and IWrapDataCollector
is no where close to that I dropped the idea.
The results will be same with an Abstract Class as these classes can't have an object.
After failing of this dreamy idea,
I tried (against all the documentations advice) to write a concrete implementation as my SPI
package com.myWrapper.test;
import com.thirdParty.library.IDataCollector;
public class IWrapDataCollector implements IDataCollector {
public Integer getMarksWrapper() {
return null;
}
public String getStudentsNameWrapper() {
return null;
}
@Override
public final String getStudentName() {
return getStudentsNameWrapper();
}
@Override
public final Integer getMarks() {
return getMarksWrapper();
}
}
I extended this concrete class and wrote 2 implementations to test out the idea. Please note that I have added third-party dependency in my end-user project at this moment and was planning to remove that once this idea goes ahead.
Yet, again, I realized how stupid this idea is. This class definitely got but only this class got loaded which is actually correct because I have registered this class and I got a much deserved NullPointerException for this.
Again, how can I achieve this and remove the dependency of third-party from the user-project by just keeping it in wrapper project.
You won't decouple your SPI from the third party if you use extends ThirdPartyXYZ
in your own interface + implementation. Your interface must be clean of the 3rd party, and the implementation calls just delegate to 3rd party calls. Then when you remove the 3rd party you won't need to totally change your interface and client code.
So you need a clean service interface IWrapDataCollector
:
package com.myWrapper.test;
public interface IWrapDataCollector {
String getStudentName();
Integer getMarks();
... reproduce all calls needed for the service, no 3rd party classes
}
Add an implemention for your wrapper. For first attempt just stub out all calls and DON'T call third party:
package com.myWrapper.impl;
// TEST WITHOUT CALLS TO THIRD PARTY:
public class MyDataCollector implements IWrapDataCollector
/* MUST HAVE public no-args constructor */
public MyDataCollector() {}
String getStudentName() { return "dummy value"; }
Integer getMarks() { return 1; }
... etc
Add a file to the jar META-INF/services/com.myWrapper.test.IWrapDataCollector
which has one line referring to your wrapper:
com.myWrapper.impl.MyDataCollector
Then you should be able to test your blank implementation with:
IWrapDataCollector impl = ServiceLoader.load(IWrapDataCollector.class);
If that works, re-implement MyDataCollector
(or add a new class and edit the META-INF file) to with calls that delegate to an instance of the 3rd party. This will access third party with service load call for the 3rd party API:
public class MyDataCollector implements IWrapDataCollector
IThirdParty delegate = ServiceLoader.load(IThirdParty.class);
public MyDataCollector() {}
String getStudentName() {
return delegate.getStudentName();
}
... etc
Your own clients should still be able to use:
IWrapDataCollector impl = ServiceLoader.load(IWrapDataCollector.class);
In due course you can eliminate use of 3rd party with a new implementation simply by editing the file META-INF/services/com.myWrapper.test.IWrapDataCollector
to contain the details of the new class name.