Инкапсулировать интерфейс стороннего поставщика услуг

  1. Я пытаюсь инкапсулировать стороннюю библиотеку, написав над ней оболочку и используя только необходимые API в соответствии с моими потребностями.

  2. Чтобы добиться этого, я создал этот проект-оболочку как отдельный проект со всеми зависимостями сторонней библиотеки.

  3. В будущем сторонняя библиотека может быть заменена на лучшую, поэтому требуется высокий уровень развязки.

  4. При этом я наткнулся на некоторые интерфейсы поставщиков услуг сторонней библиотеки, которые загружаются с помощью Java ServiceLoader где-то в библиотеке.

  5. Я хочу, чтобы пользователи моего проекта-оболочки совершенно не знали об используемой сторонней библиотеке.

  6. Чтобы добиться этого, я должен написать свой проект-оболочку таким образом, чтобы пользователям не требовалась никакая зависимость от третьих сторон.

  7. Но SPI часто требуют, чтобы их конфигурация выполнялась в файле проекта META-INF/services/{SPIName}, что в конечном итоге приводит к утечке сторонних знаний.

  8. Как я могу инкапсулировать эти сторонние SPI, чтобы конечный пользователь совершенно не знал о сторонней библиотеке?

  9. Есть ли технический термин для таких случаев? или Существует ли для этого существующее решение или шаблон проектирования?

Я попробовал пару вещей после исчерпания поиска в Интернете.

Сторонний 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.

Опять же, как я могу добиться этого и удалить стороннюю зависимость от пользовательского проекта, просто сохранив его в проекте-оболочке.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
0
74
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы не отделите свой 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?

booyaakaashaa 16.06.2024 09:35

Если в вашем коде используется сторонняя сторона, то все клиенты вашего уровня обслуживания косвенно зависят от одной и той же третьей стороны и требуют этот код в пути к классу или модулю. Однако им не потребуется перекомпилировать новую версию вашего сервиса, если вы удалите делегата.

DuncG 17.06.2024 14:09

Другие вопросы по теме