Как вы думаете, вы пишите модульные приложения? Подозреваю, что вы скажете: “Конечно! Я организую свои классы и интерфейсы с помощью пакетов. Я разбиваю свое приложение на функциональные слои, слежу за тем, чтобы связанность (coupling) была слабой, скрывая функциональность за интерфейсами”.

Допустим, что это так. Может быть вы даже используете фреймворк, основанный на шаблоне проектирования dependency injection (инъекция зависимости), такой как Spring, который позволяет сделать слои вашего приложения еще менее зависимыми друг от друга. Возможно даже, что вы разбиваете свое приложение на несколько независимых приложений, каждое из которых собирается отдельно.

Всё это очень хорошо. Но если ваше приложение такое модульное, то почему же вы до сих пор разворачиваете его как один монолитный WAR-файл?

Я утверждаю что ваше приложение совершенно не модульное.

Что значит быть Модульным?

Говоря простым языком, модуль представляет собой автономный компонент более крупной системы. Хорошо спроектированный модуль должен иметь слабую связанность (low coupling) и высокое зацепление (hight cohesion).

Суть зацепления (cohesion) в том, чтобы сделать что-то одно и сделать это хорошо. Модуль, имеющий высокое зацепление, позволяет решить только одну, узкоспециализированную задачу и не содержит ничего, что не связано с решением этой задачи. Вследствие этого, модули с высоким зацеплением, как правило, получаются компактными, надежными, их легко использовать повторно, и они легки для понимания. (В английском языке для описания модулей используется слово fine-grained, которое означает “мелкозернистый”).

Зацепление – это внутренняя метрика модуля. Связанность (coupling) является внешней метрикой модуля и отражает то, как модуль взаимодействует с другими модулями. Модули со слабой связанностью взаимодействуют с другими модулями только через интерфейсы, ничего не зная о реализации, которая за ними скрывается, вследствие чего, изменение реализации одного модуля, как правило ни как не влияет на взаимодействующие с ним модули.

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

  • Изменяемость. Если каждый модуль в приложении известен только по его открытому интерфейсу (но не по реализации), то становится легко заменить один модуль другим, предоставляющим такой же интерфейс. Можно сказать, что модульность позволяет менять наше мнение быстрее.
  • Простота восприятия. Связные модули с хорошо обозначенными границами гораздо легче изучать и они легки для понимания. Гораздо проще изучать приложение маленькими частями, что в конечном итоге приведет к глубокому пониманию всего приложения.
  • Параллельная разработка. Модули могут разрабатываться независимо друг от друга, что позволяет командам разработчиков распределять задачи по границам модулей.
  • Улучшенная тестируемость. Несмотря на то, что модульное и интеграционное тестирование являются хорошими практиками, можно улучшить качество тестирования, добавив еще один уровень – тестирование каждого модуля как единого целого.
  • Гибкость и многократное использование. В зависимости от области применения модуля и степени абстракции его функционала, становится возможным взять модуль из одного приложения и использовать его в другом приложении.

Модульность не является новой идеей. Это обычный подход в производстве всех видов систем, будь то программное обеспечение или что-то другое. Еще в 1970 году Ричард Готье и Стивен Понте в своей книге “Проектирование программных систем” привели преимущества модульности.

Большинство языков программирования в той или иной степени поддерживают модульность. Существуют даже языки, например Modula-2 и MIL, которые основаны на принципах модульности.

Но как насчет Java? Способствует ли Java разработке модульных приложений?

Модульность в Java

В Java единицей модульности обычно принято считать JAR-файл (Java Archive). К сожалению, JAR-файлы создают лишь иллюзию модульности. Обычный JAR-файл всего лишь удобен при развертывании, являясь контейнером для классов, интерфейсов и других ресурсов. Из рисунка, представленного ниже видно, что как только JAR-файл оказался в classpath, его границы растворяются вместе с грёзами о модульности.

Во время выполнения границы JAR-файла стираются

Во время выполнения границы JAR-файла стираются

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

Таким образом, хоть JAR-файлы и создают видимость поддержки модульности, их слабые границы не могут ограничить доступ к их внутренней реализации. Это делает их уязвимыми для “злоумышленников” и не защищает от сильной связанности (hight coupling).

Всё что нужно сделать – укрепить границы JAR-файла так, чтобы посторонний мог видеть и использовать только опубликованный API библиотеки. И здесь на сцену выходит OSGi.

Введение в OSGi и введение OSGi

OSGi – это спецификация компонентного фреймворка, реализация которой позволяет добиться настоящей модульности на платформе Java. OSGi позволяет создать слабую связанность и высокое зацепление. Более того, каждый модуль может быть разработан, протестирован, развернут и обновлен автономно, никак не влияя на другие модули. Давайте посмотрим на ингредиенты, из которых состоит спецификация OSGi 4.1 и узнаем, как они поддерживают разработку модульного программного обеспечения на Java.

Ключевые элементы OSGi

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

Архитектура OSGi

Архитектура OSGi

На нижнем уровне спецификация OSGi определяет модель развертывания Java-модулей. Единица развёртывания в OSGi называется бандлом (bundle – комплект, пучок). Вместо того, чтобы создавать совершенно новый механизм развёртывания, OSGi использует существующий формат JAR-файлов для бандлов. Бандлы OSGi очень похожи на обычные JAR-файлы, отличие лишь в том, что OSGi-бандл содержит файл META-INF/MANIFEST.MF, в котором хранятся специфичные для OSGi метаданные, такие как имя, версия, список зависимостей, а также некоторые детали, необходимые для развёртывания бандла.

После установки (install) бандла в OSGi-фреймворк, в дело вступает жизненный цикл OSGi (см. рисунок выше), который контролирует статус бандла. Бандл может быть установлен (installed), запущен (started), остановлен (stopped) и удален (uninstalled) из фреймворка. Эти состояния жизненного цикла определены в спецификации OSGi.

OSGi предоставляет нам реестр сервисов, с помощью которого бандлы могут публиковать услуги и/или подписываться на них, т.е. быть их потребителями, использовать их. Из рисунка, следующего за этим абзацем, можно увидеть что реестр OSGi имеет сервис-ориентированную архитектуру (SOA). Однако, в отличии от многочисленных интерпретаций SOA, которые основываются в основном на понятии web-сервиса как средства для связи, сервисы OSGi публикуют и подписываются на услуги в рамках одной виртуальной Java-машины (JVM), поэтому OSGi иногда называют “SOA в JVM”.

SOA в JVM - это OSGi!

SOA в JVM - это OSGi!

Пользуясь возможностями реестра сервисов, спецификация OSGi определяет ряд основных услуг, которые могут использоваться как самим фреймворком, так и сторонними бандлами. Спецификация определяет такие сервисы как журналирование (logging), служба HTTP, конфигурационная служба и некоторые другие.

Наконец, спецификация определяет опциональный слой безопасности (security layer), который охватывает остальные слои. Этот слой гарантирует, что бандл будет развернут только при успешном прохождении им процедуры аутентификации, которая включает проверку цифровой подписи бандла или проверку места из которого был установлен оригинальный бандл. Кроме того, слой безопасности может поддерживать права доступа в стиле Java 2 для контроля загрузки и выполнения классов бандла.

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

Сокрытие содержимого

В OSGi каждый бандл загружается в своем собственном пространстве классов (class space). Следовательно, содержимое бандла является private если не экспортируется явно. Это даёт возможность изменять или разрабатывать внутреннюю реализацию бандла не влияя на другие бандлы, которые зависят только от постоянного публичного API.

Это, как видите, кардинально отличается от того, что происходит при использовании обычных JAR-файлов, границы которых стираются во время выполнения.

Реестр сервисов

Предоставляя “SOA в JVM”, OSGi позволяет публиковать модули и зависеть от сервисов, опубликованных другими бандлами. У сервисов известны только опубликованные интерфейсы, но не их реализация. Это означает, что сохраняется слабая связанность между бандлами, публикующими свои услуги и пользующимися этими сервисами.

Параллельные версии бандлов

Так как каждый бандл работает в своём собственном пространстве классов, становится возможным одновременно использовать две или более версий одного и того же бандла в одном OSGi-фреймворке. Без OSGi, граф зависимостей, изображенный на рисунке ниже, представлял бы собой дилемму, в которой вы должны выбрать, какую версию библиотеки Zab использовать, и надеяться на то, что она будет правильно работать с обеими зависимыми библиотеками. Однако в OSGi вам не придется выбирать. Обе версии этой библиотеки могут существовать в приложении в одно и то же время, и каждый зависимый бандл может работать со своей версией библиотеки Zab.

Несколько версий бандла могут быть установлены в OSGi в одно и то же время

Несколько версий бандла могут быть установлены в OSGi в одно и то же время

Динамическая модульность

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

Строгое именование

В отличие от традиционных JAR-файлов, не обеспечивающих возможность однозначно себя идентифицировать, бандлы OSGi могут быть идентифицированы по символическому имени бандла (bundle’s symbolic name) и номеру версии в манифесте бандла.

Следует отметить, что OSGi не является серебряной пулей для модульности. Простое применение OSGi к архитектуре вашего приложения не гарантирует улучшение его модульности. Чтобы приложение было модульным, его необходимо правильно спроектировать. Это как с ООП. Java – объектно-ориентированный язык программирования, но на нём можно писать программы, совершенно не соответствующие парадигме ООП. Тем не менее, OSGi способствует тому, чтобы вы писали именно модульные приложения, позволяя вам создавать четко определенные модули.

Эта статья является вольным переводом вводной части книги Modular Java Крэйга Уолса.