Історія модульности в мові java

Припустимо, у нас є що розвивається додаток на Java з досить цікавим функціоналом. Але, на жаль, з часом стає все складніше додавати в нього нові деталі, програма починає розклеюватися в несподіваних місцях. Цілком можливо, проблема полягає в недостатній модульности додатки. Але не карайте себе - не ви в цьому винні. Просто традиційний мову Java сумно відомий як раз погано організованою модульностью. Але так бути не повинно.

Підвищуючи модульність, вдається створювати розгортаються системи, більш зручні в підтримці і компонуванні. Якщо у вас є чітко визначені межі між модулями, то все добре. Будь-які функції можна тестувати окремо від інших, на рівні коду і роботи з командою відмінно діє принцип «розділяй і володарюй». Розробка прискорюється, і така висока швидкість зберігається не тільки в перший рік існування системи, але і протягом усього її життєвого циклу.

Що таке справжня модульність? Чи є об'єкти або пакети модулями? А може це jar-файли?

Від архітектури до програм

Як же побудувати по-справжньому модульну систему? Конкретне рішення ми розглянемо нижче, а поки давайте чітко визначимо стоїть перед нами проблему. Модульність дуже важлива на багатьох рівнях абстрагування. Архітектура нашої системи влаштована за принципом «SOA» (сервіс-орієнтована архітектура). Правильно організована сервіс-орієнтована архітектура забезпечує створення явних і версіонірованних загальнодоступних інтерфейсів (в основному мова йде про веб-службах) між слабо зв'язаної системами, що приховують свої внутрішні деталі. Ці підсистеми цілком можуть працювати на основі абсолютно автономних технологічних стеків, і тому кожну з таких підсистем можна легко замінити, не зачіпаючи всі інші.

Проте при створенні окремо взятих сервісів або підсистем на Java часто не вдається позбутися від монолітного підходу. На жаль, характерним прикладом цього є середовище часу виконання мови Java, rt.jar. Зрозуміло, ви можете розбити ваше монолітне додаток на три обов'язкових рівня, але це лише жалюгідна подоба справжньої модульности. Просто запитайте: що потрібно зробити, щоб витягти з вашого додатки базовий рівень і замінити цей рівень на абсолютно іншу реалізацію? Дуже часто така заміна відбивається на всьому додатку. А тепер давайте подумаємо, як зробити це, не пошкоджуючи інших частин програми, при гарячій заміні прямо під час виконання. Адже це можливо в SOA-додатках, так чому б не реалізувати таку можливість в наших додатках?

справжня модульність

Отже, що ж таке «справжня модульність», яку ми вже злегка торкнулися в попередньому розділі? Сформулюю кілька визначальних характеристик повноцінного модуля:

  • модуль - це незалежний блок розгортання (допускає перевикористання в будь-якому контексті);
  • модуль володіє стабільними конкретними ідентифікаційними ознаками (наприклад, ім'ям і версією);
  • модуль висуває певні вимоги (наприклад, залежно);
  • один модуль може використовуватися іншими, але при цьому внутрішні деталі даного модуля залишаються для них прихованими.

В ідеалі такі модулі повинні існувати в легковагій середовищі виконання, яка поєднує вимоги і можливості модулів, вибудовуючи максимально влаштовує нас компоновку. Коротше кажучи, ми добиваємося модульности додатків, щоб взяти від сервіс-орієнтованої архітектури всі її переваги, але реалізувати їх в значно більш дрібному масштабі. Причому не тільки на дошці з маркерами, а й при написанні коду, і при експлуатації програми. Більш того, композиція модулів не повинна бути статичною: нам потрібні еластичні і розгортаються додатки, які не будуть лягати і вимагати повномасштабного переразвертиванія.

Об'єкти: це і є справжні модулі?

Звернемося до самого нижнього рівня структурних абстракцій в мові Java: класам і об'єктам. Хіба об'єктна орієнтація не забезпечує чіткої ідентифікації, приховування інформації і слабкого зв'язування - за допомогою інтерфейсів? Так, до певної міри. Але ідентичність об'єктів в даному випадку є ефемерною, а інтерфейс не версіоніровани. Поза всякими сумнівами, класи в Java не можна називати «незалежними блоками розгортання». На практиці класи зазвичай занадто тісно «знайомляться» один з одним. «Загальнодоступний» (public) означає «видимий практично будь-якого класу» в шляху (classpath), позначеному на JVM. Така відкритість є небажаною практично для будь-яких сутностей, крім по-справжньому загальнодоступних інтерфейсів. Гірше того, застосування модифікаторів видимості Java під час виконання не є строго обов'язковим (наприклад, при відображенні).

Перевикористати класи поза ними вихідного контексту складно, якщо ніхто не нав'язує вам додаткові витрати, пов'язані з неявними зовнішніми залежностями. Я так і чую, як у вас в голові зараз проносяться слова «Впровадження залежностей» і «Інверсія управління». Так, ці принципи допомагають зробити залежності класів явними. Але, на жаль, їх архетипічні реалізації в Java і раніше, змушують створювати додатки, що нагадують величезні грудки з об'єктів, статично пов'язують під час виконання і утворюють потворні конструкції. Настійно рекомендую почитати книгу «Java Application Architecture: Modularity Patterns», якщо ви дійсно хочете розібратися в патернах модульного проектування. Але ви також дізнаєтеся, що при застосуванні цих патернів в Java без обов'язкового дотримання модульности під час виконання ви відразу серйозно ускладнюєте собі роботу.

Пакети: може бути, це справжні модулі?

А що якщо істинні модулі мови Java слід шукати десь на півдорозі між додатком і об'єктами? Наприклад, чи не є пакети такими повноцінними модулями? Комбінація назв пакунків, операторів імпорту та модифікаторів видимості (наприклад, public / protected / private) створює ілюзію того, що пакети мають як мінімум деякими характеристиками модулів. На жаль, пакети залишаються чисто косметичними конструкціями, вони всього лише забезпечують простору імен для класів. Навіть їх, здавалося б, явна ієрархічність ілюзорна.

JAR-файли: справжні модулі?

В такому випадку не є справжнім модулем Java JAR-файл? І так і ні. Так - тому що JAR-файли є незалежними одиницями розгортання в Java-додатках. Ні, оскільки вони не володіють трьома іншими характеристиками. У файлі MANIFEST.MF вказується ім'я, а іноді - навіть версія JAR-файлу. Ні те ні інше не входить до складу моделі часу виконання і, отже, не може служити явною фактичної ідентифікаційної інформацією. Не можна оголошувати залежності між різними JAR-файлами. Ви самі повинні забезпечити дотримання всіх залежностей в шляху до класу. До речі, шлях - це просто плоска колекція класів: зв'язку з вихідними JAR-файлами в ньому губляться. Цим же пояснюється ще одна велика проблема: будь-який загальнодоступний клас з JAR-архіву залишається видимим в межах всього шляху до класу. Не існує модифікатора видимості «jar-scope», який дозволив би приховувати деталі реалізації всередині JAR.

Отже, JAR-файли є необхідним механізмом забезпечення модульності додатків, але одних їх недостатньо. Є фахівці, успішно вибудовують системи з численних JAR-файлів (але із застосуванням патернів модульної архітектури), які вміють керувати ідентифікаційної інформацією і залежності за допомогою улюблених інструментів, призначених саме для цього. Взяти хоча б Netflix API, що складається з 500 JAR-файлів. Але, на жаль, шлях до класу під час компіляції і виконання неодмінно буде відхилятися непередбачуваним чином, і розверзатиме JAR-ад. На жаль, з цим доводиться миритися.

набори OSGi

Отже, звичайну мову Java не може забезпечити достатньої модульности. Це визнана проблема, можливо, знайти для неї нативное рішення вдасться в рамках проекту Jigsaw. Однак він так і не увійшов до Java 8. а до цього - в Java 7, тому в найближчому майбутньому доведеться обходитися без нього. Якщо взагалі доведеться. Залишається OSGi. модульна платформа для роботи з Java, вже зріла і обкатана в бойових умовах. Вона використовується в деяких серверах додатків і в IDE і є основою для їх розширюються архітектур.

Історія модульности в мові java

Технологія OSGi додає на JVM модульність, яка стає при цьому першокласним аспектом (first-class citizen) платформи. Це робиться шляхом доповнення JAR-файлів і пакетів необхідної семантикою, що дозволяє в повній мірі реалізувати справжню модульність. Модуль OSGi, також іменований «набором» (bundle), - це JAR ++. Він визначає в маніфесті JAR додаткові поля для версії (бажано семантичної), імені набору і списку пакетів з набору, які повинні експортуватися. Експорт пакета означає, що ви привласнюєте пакету версію, все загальнодоступні класи цього пакета будуть видимі іншим наборам. Всі класи з неекспортірованних пакетів будуть видимі тільки всередині набору. OSGi забезпечує такий хід роботи під час виконання не в останню чергу завдяки тому, що у кожного набору є окремий завантажувач класів. Набір може вибрати для імпорту пакети, експортовані іншим набором - знову ж таки, визначаючи його імпортовані залежності в маніфесті JAR-файлу. Зрозуміло, при такому імпорті повинна визначатися і версія (діапазон), що дозволяє отримати розумні залежності і керувати процесом дозволу наборів в OSGi. Таким чином, у вас навіть можуть одночасно працювати кілька версій пакета і його класів. Невеликий приклад маніфесту з деякими параметрами OSGi:

Історія модульности в мові java
І відповідний маніфест для сервісного пакета:

Маємо: OSGi надає нам незалежно розгортається JAR зі стабільною ідентифікаційної інформацією, який може вимагати або пропонувати залежності (тобто версіонірованние пакети). Все інше строго укладено всередині наборів. Середовище часу виконання OSGi залагоджує всі тонкощі, необхідні для збереження чіткого поділу в ході роботи. Ви навіть можете виробляти гарячу заміну, додавання або видалення пакетів під час виконання!

сервіси OSGi

Отже, набори OSGi забезпечують дотримання залежностей, які визначаються на рівні пакета і визначають динамічний життєвий цикл для наборів, що містять ці пакети. Може бути, ось і все, що потрібно для реалізації SOA-подібного рішення в мініатюрі? Майже. Є ще одна найважливіша концепція, яка відокремлює нас від створення справжніх модульних мікросервісов на базі OSGi-наборів.

Працюючи з наборами OSGi, ми можемо програмувати в розрахунку на інтерфейс, що експортується набором. Але як отримати реалізацію цього інтерфейсу? Експорт реалізує класу - погане рішення, якщо це робиться тільки для інстанцірованія його в пакетах-споживачах. Можна було б використовувати фабричний патерн і експортувати фабрику як частина API. Але перспектива написання фабрики для кожного інтерфейсу здається кілька ... примітивною. Не підходить. Але рішення існує: треба працювати з OSGi-сервісами. OSGi надає сервісно-реєстровий механізм, що дозволяє зареєструвати вашу реалізацію з її інтерфейсом в реєстрі сервісів. Як правило, ви будете реєструвати ваш сервіс при запуску набору, що містить потрібну реалізацію. Інші набори можуть запитувати реалізацію для заданого загальнодоступного інтерфейсу з сервісного реєстру. Вони отримають реалізацію з реєстру, і для цього навіть не потрібно знати базового класу реалізації в їхньому коді. OSGi автоматично управляє залежностями між постачальниками і споживачами сервісів практично так само, як і залежності, що діють на рівні пакета.

Історія модульности в мові java
Звучить заманливо, правда? Є, правда одна невелика ложка дьогтю: виявляється, використовувати низькорівневий API сервісів OSGi складно, і для цього потрібно писати багато коду. Адже сервіси можуть з'являтися і зникати під час виконання. Справа в тому, що набори надають сервіси, які можна абсолютно довільно запускати і зупиняти, причому на таку зупинку або запуск можуть піти навіть вже працюють набори. Цей механізм має величезний потенціал, якщо ви прагнете будувати гнучкі і довговічні додатки, але як розробник ви повинні строго дотримуватися обраної стратегії. На щастя, для усунення цієї проблеми вже створено кілька високорівневих абстракцій. Такі інструменти, як Declarative Services або Felix Dependency Manager. допомагають з легкістю створювати і споживати сервіси при роботі з OSGi. Можете не дякувати за пораду.

Чи варта гра свічок?

Мабуть, ви погодитеся, що вибудовування по-справжньому модульної бази коду - це амбітна мета. І, звичайно ж, досягти цієї мети можна і без OSGi. У свою чергу, модульні середовища виконання на зразок OSGi не врятують таку базу коду, де модульностью і не пахне. Зрештою, модульність - це архітектурний принцип, який можна застосовувати практично на будь-якому матеріалі, було б бажання. Але спроектувати модульну конструкцію нелегко. Середовище часу виконання буде гладко працювати з модулями лише за умови, що модулі і їх залежності є в такому середовищі першокласними сутностями, від етапу проектування до етапу експлуатації.

На практиці все це здається складним? Так, тут, зрозуміло, є що простудіювати, але все не так страшно, як багато хто вважає. За останні кілька років інструментальна підтримка для розробки в стилі OSGi радикально покращилася. Особливої ​​згадки заслуговують bnd і bndtools. Якщо ви хочете відчути, що таке розробка повноцінних модульних додатків на Java, подивіться цей деморолик мого колеги Пола Баккера.

Модульні архітектури і варіанти проектування в даний час викликають все більший інтерес. Якщо ви зважитеся не відкладаючи випробувати такі прийоми в вашому коді на Java, то рекомендую вам ознайомитися з матеріалами, на які я поставив посилання в цій статті, і дати OSGi новий імпульс. Попереджаю: дійсно, буде непросто. Але освоїти OSGi реально, а от домогтися справжньої модульности куди складніше.

Сандер Мак

Схожі статті