Pull to refresh

Модули Java 9 и внедрение зависимостей: используем Guice

Reading time 7 min
Views 11K
Original author: Paul Bakker
Коллеги, привет!

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

Складывается впечатление, что модульная организация Java 9 потребует от программиста недюжинной изобретательности, и один из перспективных вариантов адаптации к такому дивному новому миру — это внедрение зависимостей. Именно по этому поводу внятно и интересно высказался в блоге O'Reilly уважаемый Пол Бэккер (Paul Bakker), один из авторов книги "Java 9 Modularity"


Приятного чтения и не забудьте проголосовать пожалуйста!

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

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

Рассмотрим реальный пример. В нашей книге «Java 9 Modularity» в качестве сквозного примера рассматривается приложение, анализирующее сложность заданного текста. Приложение существует в двух вариантах: как CLI (интерфейс командной строки) и GUI (графический пользовательский интерфейс). Также в нем применяются различные алгоритмы для расчета сложности текста. CLI и GUI – это два отдельных модуля, и каждый аналитический алгоритм — тоже отдельный модуль. Естественно, модули CLI и GUI зависят от анализаторов, но они должны использовать только интерфейс Analyzer. Модули CLI и GUI должны сохранять работоспособность, не имея никакой информации о реализации интерфейсов.

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

Всякий раз, разбивая подобную систему на модули, мы сталкиваемся с проблемой практического характера. Как добиться слабой связанности между CLI/GUI и анализаторами? Ведь в какой-то момент нам непременно потребуется создать экземпляр класса реализации. Классическое решение такой проблемы – внедрение зависимости, либо, иными словами, инверсия управления. Если мы воспользуемся внедрением зависимости, то наш код CLI/GUI просто объявит, что ему нужны экземпляры интерфейса Analyzer; обычно это делается при помощи аннотаций. Фактическое инстанцирование классов реализации и привязка их к коду CLI/GUI выполняется при помощи фреймворка для внедрения зависимостей – популярными примерами таких фреймворков являются Spring и Guice. В этой статье используется Guice, но в книге Java 9 Modularity также есть подробный пример на основе Spring.

Что лучше при работе с модулями: внедрение зависимостей или инкапсуляция?
Java 9 и модульная система этой версии языка позволяет вывести «развязывание» кода на новый уровень. Раньше можно было программировать в расчете на интерфейсы, но по-настоящему скрыть классы реализации не удавалось. До версии Java 9 в Java, в сущности, было невозможно инкапсулировать классы в модуле (и даже объявить модуль, если на то пошло). Ситуация меняется с появлением модульной системы Java 9, однако, здесь же возникает ряд новых проблем при работе с фреймворками для внедрения зависимостей.

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

Рассмотрим типичную настройку Guice.

public static void main(String... args) throws IOException {
      Injector injector = Guice.createInjector(
              new ColemanModule(),
              new KincaidModule(),
              new NextgenSyllableCounterModule(),
              new NaiveSyllableCounterModule()
              );
    
      CLI cli = injector.getInstance(CLI.class);
      cli.analyze(args[0]);
   }

В этом главном методе осуществляется начальная загрузка фреймворка Guice с несколькими модулями Guice (не путайте их с модулями Java 9!). В каждом модуле есть одна или более реализаций для интерфейсов, которые мы собираемся внедрять. Например, модуль ColemanModule мог бы выглядеть так.

public class ColemanModule extends AbstractModule{

    @Override
    protected void configure() {
        Multibinder.newSetBinder(binder(), Analyzer.class)
                .addBinding().to(ColemanAnalyzer.class);
    }
}

Наконец, мы определяем наш код CLI с аннотацией @Inject, сообщая Guice таким образом, что при создании экземпляра этого класса фреймворк должен внедрять зависимости.

public class CLI {

    private final Set<Analyzer> analyzers;

    @Inject
    public CLI(Set<Analyzer> analyzers) {
        this.analyzers = analyzers;
    }
    
    // остальной код опущен

Главный метод находится в модуле вместе с классом CLI. Класс реализации ColemanAnalyzer и модуль ColemanModule также совместно находятся в модуле. В идеале следовало бы инкапсулировать оба этих класса, поскольку и тот, и другой – это классы реализации. Наш CLI-модуль не должен бы напрямую от них зависеть. К сожалению, это невозможно. Нам придется экспортировать exports пакет, содержащий ColemanModule, поскольку он нужен нам для начальной загрузки Guice. Во-вторых, потребуется открыть пакет, содержащий ColemanAnalyzer, а также пакет с CLI, поскольку для инстанцирования классов Guice требуется глубокая рефлексия. Теперь у нас есть связь между модулем CLI и каждым модулем-анализатором, как показано на следующем рисунке. Это очень плохо!



Рис. 1. Зависимости между модулями: налицо сильное связывание

Свидетельствуют ли эти новые проблемы, что с модулями сложно работать? Отнюдь! Модули наконец-то позволяют нам инкапсулировать код, и это серьезный шаг к такому проектированию «со слабым связыванием», к которому мы стремимся. Существующие фреймворки не рассчитаны на эти новые возможности, поэтому, возможно, нам потребуется немного пересмотреть работу с ними. Однако, сейчас я покажу, какой великолепный обходной маневр на данный случай предлагает Guice.

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

Использование сервисов в качестве альтернативы внедрению зависимостей

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

Ниже приведен дескриптор модуля, и этот модуль объявляет, что предоставляет реализацию сервиса. Обратите внимание: Analyzer – это обычный интерфейс Java, а ColemanAnalyzer – это обычный класс Java, реализующий интерфейс Analyzer.

module easytext.analysis.coleman {
   requires easytext.analysis.api;

   provides javamodularity.easytext.analysis.api.Analyzer with  
         javamodularity.easytext.analysis.coleman.Coleman;
}

Модуль CLI обязан объявить, что использует сервис Analyzer. Также ему требуется модуль, экспортирующий интерфейс Analyzer, а модуль Coleman не требуется.

module easytext.cli {
   requires easytext.analysis.api;

   uses javamodularity.easytext.analysis.api.Analyzer;
}

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

Iterable<Analyzer> analyzers =.    
   ServiceLoader.load(Analyzer.class);

for(Analyzer analyzer: analyzers) { 
  System.out.println(analyzer.getName() + ": " +   
     analyzer.analyze(sentences));
}

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


Рис. 2. Ослабление связывания при помощи сервисов

Что, если мы все равно хотим использовать Guice, поскольку нам придется работать с уже имеющейся базой кода, основанной на Guice – либо если нам попросту нравится декларативная природа внедрения зависимостей? Можно ли лучше совместить этот фреймворк с модульной системой? Оказывается, комбинация с Guice – очень красивое решение!

Сочетание внедрения зависимостей с сервисами

Как мы уже убедились, основная проблема при работе с Guice – возникновение непосредственной связи между модулем CLI/GUI и модулями анализаторов. Все дело в том, что нам требуются классы AbstractModule для начальной загрузки Guice. Что, если бы удалось вообще исключить этот шаг и предоставлять классы AbstractModule в виде сервисов?

module easytext.algorithm.coleman {
   requires easytext.algorithm.api;
   requires guice;
   requires guice.multibindings;

   provides com.google.inject.AbstractModule with javamodularity.easytext.algorithm.coleman.guice.ColemanModule;

   opens javamodularity.easytext.algorithm.coleman;
}

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

Со стороны CLI/GUI можно сделать начальную загрузку, Guice, подыскав реализации AbstractModule при помощи ServiceLoader. Больше никакого связывания с модулями реализации!

Injector injector = Guice.createInjector(
                ServiceLoader.load(AbstractModule.class));

CLI cli = injector.getInstance(CLI.class);

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

Исходный код

Весь исходный код для этой статьи выложен на GitHub. Там две ветки: одна с использованием сервисов, как показано в последнем примере, а другая – без них.
Only registered users can participate in poll. Log in, please.
Мое послевкусие
75% Интересно. Хотелось бы увидеть на русском языке всю книгу 33
56.82% Издайте пожалуйста что-нибудь новенькое по Spring 25
44 users voted. 17 users abstained.
Tags:
Hubs:
+6
Comments 3
Comments Comments 3

Articles

Information

Website
piter.com
Registered
Founded
Employees
201–500 employees
Location
Россия