Я пытаюсь инкапсулировать стороннюю библиотеку, написав над ней оболочку и используя только необходимые API в соответствии с моими потребностями.
Чтобы добиться этого, я создал этот проект-оболочку как отдельный проект со всеми зависимостями сторонней библиотеки.
В будущем сторонняя библиотека может быть заменена на лучшую, поэтому требуется высокий уровень развязки.
При этом я наткнулся на некоторые интерфейсы поставщиков услуг сторонней библиотеки, которые загружаются с помощью Java ServiceLoader где-то в библиотеке.
Я хочу, чтобы пользователи моего проекта-оболочки совершенно не знали об используемой сторонней библиотеке.
Чтобы добиться этого, я должен написать свой проект-оболочку таким образом, чтобы пользователям не требовалась никакая зависимость от третьих сторон.
Но SPI часто требуют, чтобы их конфигурация выполнялась в файле проекта META-INF/services/{SPIName}, что в конечном итоге приводит к утечке сторонних знаний.
Как я могу инкапсулировать эти сторонние SPI, чтобы конечный пользователь совершенно не знал о сторонней библиотеке?
Есть ли технический термин для таких случаев? или Существует ли для этого существующее решение или шаблон проектирования?
Я попробовал пару вещей после исчерпания поиска в Интернете.
Сторонний SPI
package com.thirdParty.library;
public interface IDataCollector {
String getStudentName();
Integer getMarks();
}
Некоторый класс, в который загружены все поставщики услуг для этого SPI. Обратите внимание, что в реальной третьей стороне все настолько сложно, насколько это возможно.
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;
}
}
Проект-оболочка: я планировал написать интерфейс в своей оболочке IdataCollector
, а затем реализовать этот интерфейс, и я также мог зарегистрировать его в META-INF/services. Более того, я думал, что предоставлю IWrapDataCollector конечным пользователям, и они зарегистрируют свои META-INF/сервисы с помощью этого интерфейса. это происходит следующим образом:
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
с записью как com.myWrapper.test.IWrapDataCollector
, но вскоре я понял, что ServiceLoader может загружать только конкретные классы, а IWrapDataCollector
далеко не так близко к этому, и я отказался от этой идеи.
Результаты будут такими же, как и для абстрактного класса, поскольку эти классы не могут иметь объекта.
После провала этой мечтательной идеи
Я попытался (вопреки всем рекомендациям документации) написать конкретную реализацию в качестве моего 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();
}
}
Я расширил этот конкретный класс и написал две реализации, чтобы проверить эту идею. Обратите внимание, что на данный момент я добавил стороннюю зависимость в свой проект конечного пользователя и планировал удалить ее, как только эта идея будет реализована.
И все же я снова понял, насколько глупа эта идея. Этот класс определенно получился, но загрузился только этот класс, что на самом деле правильно, потому что я зарегистрировал этот класс и получил за это заслуженное исключение NullPointerException.
Опять же, как я могу добиться этого и удалить стороннюю зависимость от пользовательского проекта, просто сохранив его в проекте-оболочке.
Вы не отделите свой SPI от стороннего, если используете extends ThirdPartyXYZ
в своем собственном интерфейсе + реализации. Ваш интерфейс должен быть очищен от сторонних вызовов, а вызовы реализации просто делегируют сторонние вызовы. Тогда, когда вы удалите стороннюю программу, вам не нужно будет полностью менять интерфейс и клиентский код.
Итак, вам нужен чистый интерфейс сервиса IWrapDataCollector
:
package com.myWrapper.test;
public interface IWrapDataCollector {
String getStudentName();
Integer getMarks();
... reproduce all calls needed for the service, no 3rd party classes
}
Добавьте реализацию для своей оболочки. Для первой попытки просто заглушите все вызовы и НЕ звоните третьим лицам:
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
Добавьте в банку файл META-INF/services/com.myWrapper.test.IWrapDataCollector
, в котором есть одна строка, относящаяся к вашей обертке:
com.myWrapper.impl.MyDataCollector
Затем вы сможете протестировать свою пустую реализацию с помощью:
IWrapDataCollector impl = ServiceLoader.load(IWrapDataCollector.class);
Если это сработает, повторно реализуйте MyDataCollector
(или добавьте новый класс и отредактируйте файл META-INF) с вызовами, которые делегируют экземпляр сторонней организации. Это позволит получить доступ к третьей стороне с вызовом загрузки службы для стороннего API:
public class MyDataCollector implements IWrapDataCollector
IThirdParty delegate = ServiceLoader.load(IThirdParty.class);
public MyDataCollector() {}
String getStudentName() {
return delegate.getStudentName();
}
... etc
Ваши собственные клиенты по-прежнему смогут использовать:
IWrapDataCollector impl = ServiceLoader.load(IWrapDataCollector.class);
Со временем вы сможете исключить использование сторонних средств с помощью новой реализации, просто отредактировав файл META-INF/services/com.myWrapper.test.IWrapDataCollector
, включив в него сведения о новом имени класса.
Если в вашем коде используется сторонняя сторона, то все клиенты вашего уровня обслуживания косвенно зависят от одной и той же третьей стороны и требуют этот код в пути к классу или модулю. Однако им не потребуется перекомпилировать новую версию вашего сервиса, если вы удалите делегата.
В последней реализации MyDataCollector мы используем делегат IThirdParty =... Разве это не приведет к тому, что код моего клиента будет тесно связан со сторонней библиотекой? Я имею в виду, что если бы я когда-нибудь поменял библиотеку, они все равно бы использовали IThirdParty?