Перейти к основному содержимому

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

· 8 мин. чтения

1. Обзор

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

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

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

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

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

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

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

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

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

<packaging>pom</packaging>

<modules>
<module>servicemodule</module>
<module>consumermodule</module>
</modules>

<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

Есть несколько деталей, на которые стоит обратить внимание в определении родительского POM.

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

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

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

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

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

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

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

public interface TextService {

String processText(String text);

}

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

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

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();
}

}

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

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

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

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

Давайте добавим следующий com.foreach.consumermodule. Класс приложения :

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

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

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

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

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

hello from foreach!

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

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

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

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

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

Для этого нам необходимо:

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

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

5.1. Интерфейс общедоступной службы

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

Итак, давайте переместим TextService в новый пакет, который мы назовем com.foreach.servicemodule.external .

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

Затем аналогичным образом переместим наши сервисы LowercaseTextService и UppercaseTextService в com.foreach.servicemodule.internal.

5.3. Фабрика коммунальных услуг

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

В пакете com.foreach.servicemodule.external определим следующий класс TextServiceFactory :

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.foreach.servicemodule {
exports com.foreach.servicemodule.external;
}

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

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

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

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

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

hello from foreach!

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

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

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

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

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

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

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

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

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

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

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

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

<modules>
<module>servicemodule</module>
<module>providermodule</module>
<module>consumermodule</module>
</modules>

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

Наш интерфейс TextService вернется в com.foreach.servicemodule.

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

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

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

Как уже говорилось, модуль провайдера предназначен для наших реализаций, поэтому теперь давайте вместо этого поместим здесь LowerCaseTextService и UppercaseTextService . Мы поместим их в пакет, который назовем com.foreach.providermodule.

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

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

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

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

Далее мы рефакторим метод main() класса Application , чтобы он мог использовать класс ServiceLoader для обнаружения подходящей реализации: [](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/ServiceLoader.html)

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

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

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

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

The service LowercaseTextService says: hello from foreach!

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

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

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

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

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

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

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

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