Рубрики
Без рубрики

Стратегии проектирования для развязки модулей Java

Изучите стратегии проектирования для разделения модулей Java.

Автор оригинала: Alejandro Ugarte.

1. Обзор

Система модулей платформы Java (JPMS) обеспечивает более сильную инкапсуляцию, большую надежность и лучшее разделение проблем.

Но все эти удобные функции имеют свою цену. Поскольку модульные приложения построены на сети модулей, которые зависят от других модулей для правильной работы, во многих случаях модули тесно связаны друг с другом.

Это может привести нас к мысли, что модульность и свободная связь-это функции, которые просто не могут сосуществовать в одной и той же системе. Но на самом деле они могут!

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

2. Родительский Модуль

Чтобы продемонстрировать шаблоны проектирования, которые мы будем использовать для разделения модулей Java, мы создадим демонстрационный многомодульный проект Maven.

Чтобы код был простым, проект изначально будет содержать два модуля Maven, и каждый модуль Maven будет обернут в модуль Java .

Первый модуль будет включать в себя интерфейс службы, а также две реализации – поставщиков услуг. Второй модуль будет использовать провайдеров для анализа значения String .

Давайте начнем с создания корневого каталога проекта с именем demo project , и мы определим родительский POM проекта:

pom


    servicemodule
    consumermodule

    

    
        
            
                org.apache.maven.plugins
                maven-compiler-plugin
                3.8.1
                
                    11
                    11
                
            
        
    

Есть несколько деталей, которые стоит подчеркнуть в определении родительского ПОМ.

Во-первых, файл включает в себя два дочерних модуля , о которых мы упоминали выше , а именно сервисный модуль и потребительский модуль (мы подробно обсудим их позже).

Далее, поскольку мы используем Java 11, нам понадобится по крайней мере Maven 3.5.0 в нашей системе , поскольку Maven поддерживает Java 9 и выше начиная с этой версии .

Наконец, нам также понадобится, по крайней мере, версия 3.8.0 плагина компилятора Maven . Итак, чтобы убедиться, что мы в курсе событий, мы проверим Maven Central на наличие последней версии плагина компилятора Maven.

3. Сервисный Модуль

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

Давайте сделаем интерфейс службы и поставщиков услуг общедоступными , поместив их в один пакет и экспортировав все из них. Это кажется довольно хорошим выбором дизайна, но, как мы вскоре увидим, это значительно повышает уровень связи между модулями проекта.

В корневом каталоге проекта мы создадим каталог service module/src/main/java . Затем нам нужно определить пакет com.baeldung.servicemodule и поместить в него следующее TextService интерфейс:

public interface TextService {
    
    String processText(String text);
    
}

Интерфейс Text Service очень прост, поэтому давайте теперь определим поставщиков услуг.

В том же пакете давайте добавим реализацию в нижнем регистре :

public class LowercaseTextService implements TextService {

    @Override
    public String processText(String text) {
        return text.toLowerCase();
    }
    
}

Теперь давайте добавим реализацию В верхнем регистре :

public class UppercaseTextService implements TextService {
    
    @Override
    public String processText(String text) {
        return text.toUpperCase();
    }
    
}

Наконец, в каталоге service module/src/main/java давайте включим дескриптор модуля, module-info.java :

module com.baeldung.servicemodule {
    exports com.baeldung.servicemodule;
}

4. Потребительский Модуль

Теперь нам нужно создать потребительский модуль, который использует одного из поставщиков услуг, созданных нами ранее.

Давайте добавим следующий модуль com.baeldung.consumer. Применение класс:

public class Application {
    public static void main(String args[]) {
        TextService textService = new LowercaseTextService();
        System.out.println(textService.processText("Hello from Baeldung!"));
    }
}

Теперь давайте включим дескриптор модуля, module-info.java, в корне источника, который должен быть потребительский модуль/src/main/java :

module com.baeldung.consumermodule {
    requires com.baeldung.servicemodule;
}

Наконец, давайте скомпилируем исходные файлы и запустим приложение либо из нашей среды IDE, либо из командной консоли.

Как и следовало ожидать, мы должны увидеть следующий результат:

hello from baeldung!

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

Поскольку мы делаем поставщиков видимыми для внешнего мира, потребительские модули знают о них.

Более того, это борется с тем, чтобы программные компоненты зависели от абстракций.

5. Фабрика Поставщика услуг

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

Чтобы достичь этого, нам нужно:

  1. Поместите интерфейс службы в отдельный пакет, который экспортируется во внешний мир
  2. Поместите поставщиков услуг в другой пакет, который не экспортируется
  3. Создайте класс фабрики, который будет экспортирован. Потребительские модули используют класс factory для поиска поставщиков услуг

Мы можем концептуализировать описанные выше шаги в виде шаблона проектирования: интерфейс государственных услуг, частные поставщики услуг и фабрика поставщиков государственных услуг .

5.1. Интерфейс государственной службы

Чтобы четко увидеть, как работает этот шаблон, давайте разместим интерфейс службы и поставщиков услуг в разных пакетах. Интерфейс будет экспортирован, но реализации поставщика не будут.

Итак, давайте переместим Текстовый сервис в новый пакет, который мы назовем com.baeldung.service module.external .

5.2. Частные Поставщики услуг

Затем давайте аналогично переместим наш Текстовый сервис в нижнем регистре и Текстовый сервис в верхнем регистре в com.baeldung.service module.internal.

5.3. Фабрика Поставщиков Государственных Услуг

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

В пакете com.baeldung.service module.external давайте определим следующее Фабрика текстовых услуг класс:

public class TextServiceFactory {
    
    private TextServiceFactory() {}
    
    public static TextService getTextService(String name) {
        return name.equalsIgnoreCase("lowercase") ? new LowercaseTextService(): new UppercaseTextService();
    }
    
}

Конечно, мы могли бы сделать фабричный класс немного более сложным. Однако, чтобы все было просто, поставщики услуг просто создаются на основе значения String , переданного методу getTextService () .

Теперь давайте заменим наш module-info.java файл для экспорта только нашего внешнего пакета:

module com.baeldung.servicemodule {
    exports com.baeldung.servicemodule.external;
}

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

5.4. Класс Приложения

Теперь давайте рефакторингуем класс Application , чтобы он мог использовать класс фабрики поставщика услуг:

public static void main(String args[]) {
    TextService textService = TextServiceFactory.getTextService("lowercase");
    System.out.println(textService.processText("Hello from Baeldung!"));
}

Как и ожидалось, если мы запустим приложение, мы увидим тот же текст, распечатанный на консоли:

hello from baeldung!

Сделав интерфейс сервиса общедоступным, а поставщиков услуг частными, мы фактически позволили нам отделить сервисные и потребительские модули с помощью простого заводского класса.

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

6. Сервисные и потребительские модули

JPMS обеспечивает поддержку сервисных и потребительских модулей из коробки, через обеспечивает…с и использует директивы.

Поэтому мы можем использовать эту функциональность для развязки модулей, без необходимости создавать дополнительные заводские классы.

Чтобы объединить сервисные и потребительские модули, нам нужно сделать следующее:

  1. Поместите интерфейс службы в модуль, который экспортирует интерфейс
  2. Поместите поставщиков услуг в другой модуль – поставщики экспортируются
  3. Укажите в дескрипторе модуля поставщика, что мы хотим предоставить Текстовую службу реализацию с предоставляет…с директивой
  4. Поместите класс Application в свой собственный модуль – модуль-потребитель
  5. Укажите в дескрипторе модуля модуля-потребителя, что модуль является модулем-потребителем с директивой uses
  6. Используйте API Service Loader в модуле consumer для поиска поставщиков услуг

Этот подход очень эффективен, поскольку он использует всю функциональность, которую предоставляют сервисные и потребительские модули. Но это тоже довольно сложно.

С одной стороны, мы делаем так, чтобы потребительские модули зависели только от интерфейса сервиса, а не от поставщиков услуг. С другой стороны, мы даже можем вообще не определять поставщиков услуг, и приложение все равно будет компилироваться .

6.1. Родительский Модуль

Чтобы реализовать этот шаблон, нам нужно будет провести рефакторинг родительского POM и существующих модулей.

Поскольку интерфейс службы, поставщики услуг и потребитель теперь будут жить в разных модулях, нам сначала нужно изменить раздел родительского POM , чтобы отразить эту новую структуру:


    servicemodule
    providermodule
    consumermodule

6.2. Сервисный Модуль

Наш Текстовый сервис интерфейс вернется в модуль com.baeldung.service.

И мы соответствующим образом изменим дескриптор модуля:

module com.baeldung.servicemodule {
    exports com.baeldung.servicemodule;
}

6.3. Модуль Провайдера

Как уже говорилось, модуль провайдера предназначен для наших реализаций, поэтому давайте теперь разместим Текстовый сервис в нижнем регистре и U ppercaseTextService здесь. Мы поместим их в пакет, который назовем модулем com.baeldung.provider.

Наконец, давайте добавим module-info.java файл:

module com.baeldung.providermodule {
    requires com.baeldung.servicemodule;
    provides com.baeldung.servicemodule.TextService with com.baeldung.providermodule.LowercaseTextService;
}

6.4. Потребительский модуль

Теперь давайте проведем рефакторинг потребительского модуля. Во-первых, мы поместим Приложение обратно в com.baeldung.consumer модуль пакет.

Затем мы проведем рефакторинг метода Application класса main () , чтобы он мог использовать класс ServiceLoader для обнаружения соответствующей реализации:

public static void main(String[] args) {
    ServiceLoader services = ServiceLoader.load(TextService.class);
    for (final TextService service: services) {
        System.out.println("The service " + service.getClass().getSimpleName() + 
            " says: " + service.parseText("Hello from Baeldung!"));
    }
}

Наконец, мы проведем рефакторинг module-info.java файл:

module com.baeldung.consumermodule {
    requires com.baeldung.servicemodule;
    uses com.baeldung.servicemodule.TextService;
}

Теперь давайте запустим приложение. Как и ожидалось, мы должны увидеть следующий текст, распечатанный на консоли:

The service LowercaseTextService says: hello from baeldung!

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

Потребительские модули зависят от абстракций, и также легко переключаться на разных поставщиков услуг во время выполнения .

7. Заключение

В этом уроке мы узнали, как реализовать два шаблона для развязки модулей Java.

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

Конечно, у каждого из них есть свои плюсы и минусы. С первым мы получаем хорошую развязку, но нам нужно создать дополнительный заводской класс.

Со вторым, чтобы отделить модули, мы должны создать дополнительный модуль абстракции и добавить новый уровень косвенности с API загрузчика служб.

Как обычно, все примеры, показанные в этом руководстве, доступны на GitHub. Обязательно ознакомьтесь с образцом кода для шаблонов Service Factory и Provider Module .