Să presupunem că avem o aplicație Java în curs de dezvoltare cu funcționalități destul de interesante. Dar, din nefericire, cu timpul devine mult mai dificil de adăugat noi detalii, programul începe să se desprindă în locuri neașteptate. Destul de probabil, problema constă în modularitatea insuficientă a aplicației. Dar nu te executa - nu esti tu de vina. Pur și simplu, limba tradițională Java este notorie pentru modularitatea prost organizată. Dar nu ar trebui să fie așa.
Creșterea modularității, este posibil să se creeze sisteme extensibile, mai convenabile în ceea ce privește suportul și aspectul. Dacă aveți limite clar definite între module, atunci totul este bine. Orice funcții pot fi testate separat de altele, principiul "divide și cuceri" funcționează bine la nivel de cod și lucrul în echipă. Dezvoltarea se accelerează și o astfel de viteză ridicată este menținută nu numai în primul an al existenței sistemului, ci pe tot parcursul ciclului său de viață.
Ce este modularitatea adevărată? Sunt modulele de obiecte sau pachete? Poate fi dosare de borcan?
De la arhitectură la programe
Cum de a construi un sistem cu adevărat modular? Vom lua în considerare decizia concretă de mai jos, dar pentru moment să definim în mod clar problema cu care ne confruntăm. Modularitatea este foarte importantă la multe niveluri de abstractizare. Arhitectura sistemului nostru se bazează pe principiul "SOA" (arhitectură orientată spre servicii). O arhitectură orientată spre servicii organizată în mod corespunzător asigură crearea unor interfețe publice explicite și versiuni (în principal servicii web) între sistemele cu cuplaj liber care ascund părțile lor interne. Aceste subsisteme pot funcționa perfect pe baza unor stive tehnologice complet autonome și, prin urmare, fiecare dintre aceste subsisteme poate fi ușor înlocuit fără a afecta toate celelalte.
Cu toate acestea, în cazul în care creați servicii individuale sau subsisteme în Java de multe ori nu pot scăpa de abordare monolitic. Din păcate, un exemplu tipic al acestui lucru este timpul de rulare Java, rt.jar. Desigur, puteți rupe aplicația monolitic în trei nivel obligatoriu, dar este doar o aparență jalnică a adevăratei modularitate. Întrebați-vă singur: ce va fi necesar pentru a extrage nivelul de bază din aplicația dvs. și pentru a înlocui acest nivel cu o implementare complet diferită? Foarte adesea, o astfel de schimbare se reflectă în întreaga aplicație. Și acum să ne gândim cum să facem acest lucru fără a deteriora restul aplicației, cu dreptul de înlocuire la momentul executării. La urma urmei, acest lucru este posibil în aplicațiile SOA, de ce să nu implementăm acest lucru în aplicațiile noastre?
Modularitate reală
Deci, ce este "modularitatea adevărată", pe care am abordat-o deja în secțiunea anterioară? Voi formula mai multe caracteristici definitorii ale unui modul cu drepturi depline:
- Modulul este o unitate independentă de implementare (care permite reutilizarea în orice context);
- Modulul are caracteristici specifice de identificare specifice (de exemplu, nume și versiune);
- modulul prezintă anumite cerințe (de exemplu dependențele);
- un modul poate fi utilizat de alții, dar părțile interne ale acestui modul rămân ascunse pentru ele.
În mod ideal, astfel de module ar trebui să existe într-un mediu ușor de execuție care să combine cerințele și capacitățile modulelor, organizând aspectul care ne este cel mai potrivit. Pe scurt, căutăm modularitatea aplicațiilor pentru a-și lua toate avantajele de la o arhitectură orientată spre servicii, dar pentru a le realiza pe o scară mult mai mică. Și nu numai pe placa cu marcatori, dar și atunci când scrieți codul și când utilizați aplicația. Mai mult, compoziția modulelor nu ar trebui să fie statică: avem nevoie de aplicații elastice și extensibile care să nu coboare și necesită o re-implementare pe scară largă.
Obiecte: sunt aceste module reale?
Să ne uităm la cel mai scăzut nivel de abstracții structurale în limba Java: clase și obiecte. Orientarea obiectului nu oferă o identificare clară, ascunderea informațiilor și legarea slabă - folosind interfețe? Da, într-o anumită măsură. Dar identitatea obiectelor în acest caz este efemeră, iar interfețele nu sunt variante. Fără îndoială, clasele Java nu pot fi numite "blocuri independente de implementare". În practică, clasele sunt de obicei prea "familiarizate" între ele. "Public" înseamnă "vizibil pentru aproape orice clasă" în calea (classpath), indicată pe JVM. O astfel de deschidere este nedorită pentru aproape orice entitate, cu excepția unor interfețe cu adevărat publice. Mai rău, utilizarea modificatorilor de vizibilitate Java la timpul de execuție nu este strict necesară (de exemplu, în reflecție).
Este dificil să reutilizați clase în afara contextului original, cu excepția cazului în care unul vă impune costuri suplimentare cu dependințe externe implicite. Pot auzi doar cuvintele "Impunerea dependenței" și "Inversiunea conducerii", înjurătoare în mintea ta. Da, aceste principii ajută la explicarea dependențelor de clasă. Dar, din păcate, implementările lor arhetipale în Java sunt totuși forțate să creeze aplicații asemănătoare unor chestii enorme de obiecte legate în mod static în timpul execuției și formând structuri urâte. Vă recomandăm foarte mult să citiți cartea "Java Application Architecture: Modularity Patterns", dacă într-adevăr doriți să înțelegeți modelele de design modular. Dar, de asemenea, aflați că, atunci când aplicați aceste modele în Java fără a păstra neapărat modularitatea la timpul de execuție, vă complicați imediat serios munca voastră.
Pachete: poate că acestea sunt module reale?
Și dacă în cazul în care modulele Java autentice ar trebui să fie căutate undeva la jumătatea distanței dintre aplicație și obiecte? De exemplu, pachetele nu sunt astfel de module complete? Combinația dintre nume de pachete, operatori de import și modificatori de vizibilitate (de exemplu, public / protejat / privat) creează iluzia că pachetele au cel puțin unele caracteristici ale modulelor. Din păcate, pachetele rămân construcții pur cosmetice, ele oferă numai spații de nume pentru clase. Chiar ierarhia aparent ierarhică este iluzorie.
JAR fișiere: module reale?
În acest caz, fișierul JAR este un modul Java real? Și da, și nu. Da - deoarece fișierele JAR sunt unități independente de implementare în aplicații Java. Nu, pentru că nu au celelalte trei caracteristici. Fișierul MANIFEST.MF specifică numele și, uneori, chiar și versiunea fișierului JAR. Nici nu face parte din modelul timpului de execuție și, prin urmare, nu poate servi ca informații explicite de identificare factuale. Nu puteți declara dependențe între diferite fișiere JAR. Trebuie să vă asigurați că toate dependențele sunt menținute în calea clasei. Apropo, calea este doar o colecție plată de clase: link-urile către fișierele JAR originale din ea sunt pierdute. Aceasta explică, de asemenea, o altă problemă importantă: orice clasă publică din arhiva JAR rămâne vizibilă în întreaga cale spre clasă. Nu există niciun modificator de vizibilitate care să ascundă detaliile implementării în JAR.
Deci, fișierele JAR sunt un mecanism necesar pentru a asigura modularitatea aplicațiilor, dar unele nu sunt suficiente. Există specialiști care construiesc cu succes sisteme din mai multe fișiere JAR (dar cu modele de arhitectură modulară) care pot gestiona informații de identitate și dependențe cu ajutorul instrumentelor preferate destinate acestui scop. Luați cel puțin Netflix API, care conține 500 de fișiere JAR. Dar, din nefericire, calea spre clasă la timpul de compilare și execuție se va abate invariabil într-un mod imprevizibil, iar JAR-iadul se va deschide. Din păcate, acest lucru trebuie să fie tolerat.
Seturi OSGi
Deci, limba obișnuită Java nu poate oferi suficientă modularitate. Este o problemă recunoscută, probabil, să găsească o soluție alternativă ei va fi posibil, în cadrul Jigsaw. Cu toate acestea, el nu a intrat în Java 8. Înainte de aceasta - în Java 7, astfel încât în viitorul apropiat, va trebui să se descurce fără el. Dacă e deloc. Există OSGi. o platformă modulară pentru a lucra cu Java, condiții deja mature și alergate. Este folosit în unele servere de aplicații și în IDE și este baza pentru arhitecturile lor extensibile.
Tehnologia OSGi adaugă modularitatea la JVM, care devine cetățean de primă clasă al platformei. Acest lucru se face prin adăugarea de fișiere și pachete JAR cu semantica necesară, ceea ce face posibilă implementarea completă a modularității reale. Modulul OSGi, denumit și "pachet", este JAR ++. Definește câmpuri suplimentare pentru versiunea (de preferință semantică) din manifestul JAR, numele setului și lista de pachete din setul care ar trebui exportat. Exportând un pachet înseamnă că atribuiți o versiune pachetului, toate clasele publice ale acestui pachet vor fi vizibile pentru alte seturi. Toate clasele din pachetele neexpuse vor fi vizibile numai în set. OSGi oferă acest flux de lucru în timpul execuției, nu în ultimul rând datorită faptului că fiecare set are un încărcător de clasă separat. Setul poate alege să importe pachete exportate de un alt set - din nou, prin definirea dependențelor sale importate din manifestul de fișiere JAR. Desigur, cu acest import, trebuie determinată și versiunea (intervalul), ceea ce face posibilă obținerea dependențelor rezonabile și ghidarea procesului de rezolvare a seturilor în OSGi. Astfel, puteți rula chiar și mai multe versiuni ale pachetului și clasele sale în același timp. Un exemplu mic al unui manifest cu câțiva parametri OSGi: Și manifestarea corespunzătoare pentru pachetul de servicii:
Avem: OSGi ne furnizează un JAR independent, cu informații de identificare stabile care pot necesita sau oferi dependențe (adică pachete versiuni). Toate restul sunt strict închise în kituri. Mediul de funcționare OSGi rezolvă toate subtilitățile necesare pentru a menține o diviziune clară în cursul lucrării. Puteți chiar să efectuați schimburi fierbinți, adăugând sau eliminând pachete în timpul rulării!
Servicii OSGi
Astfel, setările OSGi asigură că dependențele sunt definite la nivelul pachetului și determină ciclul de viață dinamic pentru seturile care conțin aceste pachete. Poate că este doar nevoie pentru a implementa o soluție de tip SOA în miniatură? Aproape. Există un concept mai important care ne separă de crearea de microservicii modulare reale bazate pe seturi OSGi.
Lucrând cu seturi OSGi, putem programa pe baza interfeței exportate de set. Dar cum să obțineți implementarea acestei interfețe? Exportarea clasei de implementare este o decizie proastă dacă acest lucru se face numai pentru a-l instanțializa în pachete de consum. Puteți utiliza modelul fabricii și puteți exporta fabrica ca parte a API-ului. Dar perspectiva de a scrie o fabrică pentru fiecare interfață pare oarecum ... primitivă. Nu e bine. Dar există o soluție: trebuie să lucrați cu serviciile OSGi. OSGi oferă un mecanism de registru de servicii care vă permite să vă înregistrați implementarea cu interfața sa în registrul de servicii. De regulă, vă veți înregistra serviciul când porniți un set care conține implementarea dorită. Alte seturi pot solicita implementarea pentru o anumită interfață publică din registrul de servicii. Aceștia vor primi implementarea din registru, iar pentru aceasta nici măcar nu trebuie să cunoașteți clasa de implementare de bază în codul lor. OSGi gestionează automat dependențele dintre furnizorii de servicii și consumatorii de servicii în același mod ca și dependențele care acționează la nivelul pachetului.
Sună tentant, nu-i așa? Există totuși o mică zbura în unguent: se dovedește că este dificil să se utilizeze API-ul de nivel scăzut al serviciilor OSGi și pentru aceasta este necesar să se scrie o mulțime de cod. La urma urmei, serviciile pot apărea și pot dispărea în timpul execuției. Faptul este că seturile oferă servicii care pot fi complet arbitrare pornite și oprite și chiar seturile existente pot ajunge la o astfel de oprire sau a începe. Acest mecanism are un mare potențial dacă doriți să construiți aplicații flexibile și durabile, dar ca dezvoltator trebuie să respectați cu strictețe strategia aleasă. Din fericire, mai multe abstracții la nivel înalt au fost deja create pentru a elimina această problemă. Instrumente cum ar fi Serviciile Declarative sau Felix Dependency Manager. vă ajuta să creați și să consumați cu ușurință servicii atunci când lucrați cu OSGi. Nu pot mulțumi pentru sfaturi.Este jocul în valoare de lumânare?
Poate că veți fi de acord că construirea unei baze de cod modulare cu adevărat modulare este un obiectiv ambițios. Și, bineînțeles, puteți atinge acest obiectiv fără OSGi. La rândul său, mediile de execuție modulare precum OSGi nu vor salva o astfel de bază de cod în care modularitatea nu miroase. În final, modularitatea este un principiu arhitectural care poate fi aplicat aproape oricăror materiale, ar exista o dorință. Dar proiectarea unui design modular nu este ușor. Mediul runtime va funcționa fără probleme cu modulele numai dacă modulele și dependențele lor sunt în acest mediu entități de primă clasă, de la faza de proiectare până la faza operațională.
Pare totul dificil în practică? Da, aici, desigur, este ceva de studiu, dar totul nu este la fel de înfricoșător cum cred mulți. În ultimii ani, sprijinul instrumental pentru dezvoltarea în stilul OSGi sa îmbunătățit radical. Ar trebui menționate în mod special bnd și bndtools. Dacă doriți să simțiți ce este dezvoltarea de aplicații modulare cu drepturi depline în Java, uitați-vă la demo-ul colegului meu Paul Bakker.
Arhitecturile modulare și opțiunile de proiectare sunt în prezent de interes sporit. Dacă decideți să nu amânați testarea acestor trucuri în codul dvs. Java, vă recomandăm să citiți materialele la care am făcut legătura în acest articol și să oferiți un nou impuls OSGi. Te avertizez: într-adevăr, nu va fi ușor. Dar pentru a stăpâni OSGi este reală, dar pentru a obține o modularitate adevărată este mult mai dificilă.