K napsání tohoto
článku mne přivedla situace která je denním chlebem
vývojářů kteří nemají pod kontrolou infrastrukturu,
kde aplikace poběží a přesto musí docílit maximální
dostupnosti a funkčnosti aplikace – při řešení
problémů vývoje si pak nad radami typu „přejděte na
vyšší verzi nástroje XXX“ mohou jen povzdechnout. A
to proto, že nová verze nástroje XXX vyžaduje novou
verzi potřebné knihovny YYY, která ovšem běží pouze
pod JDK verze ZZZ se kterou ovšem nepracuje Váš
aplikační server SSS…
Obdobně jsou na
tom ti znás, kteří vyvíjejí aplikace resp. knihovny,
které mají dodávat zaručenou, přesto však optimální
funkčnost nezávisle na verzi JDK, aplikačního
serveru, databáze či jiné komponenty kterou
používají/v jejímž kontextu jsou nuceny
pracovat.
Podobných situcí
je samozřejmě více i z jiných oblastí.
Všichni jistě
víme, že jediná jistota v životě vývojáře je změna.
Je tomu tak i u projektů, vyvíjených na platformě
Java. Někdy se mi ale z pohledu člověka, zodpovědného
donedávna za vývoj projektu pro aplikační server
Websphere v5.1 a tudíž pro cílové JDK 1.4.x zdá, že i
sám Sun k tomuto trendu přispívá – nové verze JDK i
dalších nástrojů z pohledu mnoha lidí přicházejí
relativně vzato až příliš často a v praxi se člověk
nemá možnost ve změnách, jejich smyslu a vzájemné
kompatibilitě dost dobře orientovat. Kromě JDK každý
javovský projekt používá i další knihovny třetích
výrobců – modularita i znovupoužitelnost, kterou
všichni tak rádi (mne nevyjímaje) vzýváme, vede k
drastickému nárůstu komplexnosti závislosti na
nástrojích které sami nejsme s to plně kontrolovat
ani co do kvality, ani co do kompatibility s
prostředím kde se náš vlastní kód bude
spouštět.
Asi i to
je důvodem faktu, že vývojáři aplikací, kde je dáván
velký důraz na stabilitu a zároveň nemají vliv na
infrastrukturu, kde jejich projekt pobeží, připadají
jako dávno mrtví dinosauři. S lítostí, závistí či
skleslostí sledují, jak se svět kolem nich mění, ale
oni jsou nuceni stále vyvíjet pro JDK 1.4, 1.3 nebo
ještě starší. Musí stále spoustu věcí dělat sami
znovu, ačkoliv by bylo možno již existující funkčnost
nových knihoven využít – kdyby jen byla (s jistotou)
kompatibilní s jejich prostředím… Oné jistoty se
nedocílí nikdy, pokud nebude garantována výrobci
nástrojů které používáme – a Ti ovšem trpí podobnými
problémy jako Ti, kdo jejich produkty používají. Je
to začarovaný kruh a problém se bude v dalších letech
s přibývajícími verzemi a nástroji stále zvyšovat a
zvyšovat.
Naproti
tomu se zdá, že od zavedení prvních verzí Javy se pro
možnosti vývoje kódu kompatibilního přes více verzí
používaných knihoven (ať už JDK nebo jiných) na
úrovni jazyka až tak moc neudělalo. Možná by bylo
záhodno se tímto problémem zabývat – v diskusi by
podle mého názoru stálo za úvahu následující:
- Dát
větší důraz na oddělení vývoje „vlastního jazyka“
od „knihoven“ které ho používají – aktivity v tomto
směru se v poslední době objevují, viz např. snaha
o zavedení „Java kernelu“ (viz http://weblogs.java.net/blog/enicholas/archive/2006/09/java_browser_ed.html),
momentálně zamýšleného především pro aplety,
nicméně pokud se nebude instalovat při instalaci
Javy i celý balík všech možných knihoven, mohlo by
to možná umožnit flexibilnější instalaci různých
potřebných implementací těchto knihoven - Změny
ve vlastních rysech jazyka dělat méně často a
výraznější spíše než časté a menší, redukovat počty
nutných záplat poté co se již oficiální verze
uvolní - Zavést
do jazyka Java další nástroje a konstrukty,
umožňující co nejsnadnější implementaci
kompatibility a ověření kompatibility s různými
verzemi knihoven pro dané prostředí
Pokud se
týká prvních dvou bodů, jsou víceméně organizační
povahy. Třetí bod ale je již o konkrétních
technologických postupech. Už dnes existují některé
postupy a praktiky jak problém řešit. Mezi nimi mi
ale celkem schází výraznější podpora již velmi staré
techniky, umožňující jednoduché zvládnutí
kompatibility alespoň na úrovni zdrojových kódů –
pomocí direktiv kompilátoru pro podmíněný překlad.
Zatím se věci, které by tato technika celkem snadno
vyřešila, obchází více či méně nevhodnými
workaroundy, které lze rozdělit do několika skupin.
Jeden
využívá optimalizačního rysu Javy v případě příkazu
if který vyhodnocuje konstantní výraz, popsaného ve
specifikaci jazyka např. na
http://java.sun.com/docs/books/jls/third_edition/html/statements.html#14.21
– nejenže
jde o velmi nepřehlednou praktiku, ale neřeší
případy, kdy není možno normální „běhový“ kód na
místo podmínky vůbec napsat – např. případ
podmíněného importu. Problém je i v případě
podmíněných vnitřních tříd nebo metod – teoreticky je
sice možno uvažovat o použití anonymních tříd, ale
dělat vnitřní struktury implementací přes anonymní
třídy z důvodů podmíněné kompilace – pokud je to
vůbec možné, pak z toho běhá mráz po zádech.
Jiný de
facto simuluje preprocesor známý z jazyka C – od
zajímavého přímého použití C preprocesoru pro
přípravu javovských zdrojáků, který jsem neměl
možnost vyzkoušet, uvedeném (kromě jiných možností
workaroundů pro podmíněnou kompilaci) na
http://www.jguru.com/faq/view.jsp?EID=58973
až po
možnosti vytvoření nějakých „custom builderů“
registrovaných ve vývojových nástrojích např. v
prostředí Eclipse na úrovni každého projektu
implementujících vlastní „předprocesení“. Jeden,
podle mého sice celkem omezující, zato však
jednoduchý způsob je popsán např, na
http://weblogs.java.net/blog/schaefa/archive/2005/01/how_to_do_condi_1.html
– přikláněl
bych se ale raději než autor přímo k odstraňování
kusů kódu než jejich zakomentovávání do různých druhů
komentářových závorek, které není příliš
univerzální.
Třetím
docela univerzálním receptem (umožňujícím podmiňovat
i vnitřní strukturu tříd, importy, apod) je mnohdy
dosti nepohodlné přesouvání funkčnosti do speciálních
tříd zapouzdřujících funkčnost, která se má lišit. V
praxi to vede buďto k duplikaci kódu nebo k vytváření
shluků umělých metod s velkým množstvím parametrů –
první znamená odstraňovat algoritmické chyby ve všech
verzích, druhá brání přehlednosti kódu. Celkově je
také otázka jak specifikovat danou konkrétní
implementaci pro vytaženou funkčnost – buďto je
člověk konfrontován s hromadou jarů, obsahujících jen
specifické třídy pro danou verzi prostředí, nebo musí
už v obecném kódu takové třídy s metodami se
specifickou implementací instancovat pres reflexi a
tím pádem vědět o možných implementacích (třídy pro
různé implementace jmenují různě), což také není
ideální, nebo použít nějaký druh parametrizace a
instancování objektů přes parametrizovanou factory
(factory nemá objekty přímo zadrátované, ale jsou
aliasované a je třeba z nějakého konfiguračního
souboru vyčíst jaká implementace se má zvolit).
Derivátem
třetí možnosti s podobnými problémy je řešit problém
pomocí použití vzoru „strategie“, která se však v
mnoha případech může zdát „kanónem na vrabce“,
spojeným s příliš velkým úsilím pro jeho praktické
použití – pokud ho clovek nepoužívá od sameho
začátku, musí kód dost předělávat – nemluvě o určité
neprůhlednosti, kterou tento vzor někdy přináší…
Osobně se
asi přikláním k třetí možnosti (popř. čtvrté, resp.
jejich kombinaci), i když to může znamenat nějakou
počáteční práci – udělat si framework který bude
registry objektů spravovat a objekty na základě
konfigurace instancovat (dají se také určitě použít
služby JNDI, vytvorit různé implementace interfacu
javax.naming.ObjectFactory, apod). Tím dojde de facto
k vytvoření/použití univerzálního registru, nebo
dokonce přímo univerzální factory (v případě instancí
obsahujících pouze funkčnost a nikoliv data je možno
objekty bez problémů instancovat standardním
bezparametrickým konstruktorem). Univerzální factory
se pak může vložit do „obyčejné“ factory aby bylo
dosaženo typové kontroly a aliasy byly jen vnitřní
věcí této „specifické“ factory.
Člověk má sice práci s definicí a validací
konfiguračních souborů – ty ale může měnit alespoň
bez nutnosti rekompilace obecných zdrojů.
Nicméně –
vysoce flexibilní podmíněná kompilace klasického
rázu, jak je známa třeba z Pascalu to prostě není a
podle mého názoru by se hodila minimálně jako doplněk
stávajících možností. Pokud jste se s Pascalem
(Delphi) nesetkali, vypadá to asi následovně (znaky
„{“ a „}“ označují v pascalu komentář):
unit MyUnit;
{$DEFINE ENVIRONMENT_VER_1}
…
uses
{$IFDEF
ENVIRONMENT_VER_1}
Env_Ver_1_Lib;
{$ELSE}
Env_Universal_Lib;
{$ENDIF}
…
begin
{$IFDEF
ENVIRONMENT_VER_1}
x := Env_Ver_1_Lib_Value;
// x from Env_Ver_1_Lib
{$ELSE}
x :=
Env_Universal_Lib_Value; // x from Env_Universal_Lib
{$ENDIF}
end;
– sekce
uses odpovida Javovským importům.
Vtip je v tom, že definovat novou proměnnou není
možné jen uprostřed zdrojů přes {$DEFINE
ENVIRONMENT_VER_1}, což by mělo význam jen v
kombinaci s nějakým includem souboru s těmito
proměnnými, ale je možné ten identifikátor
zadefinovat i v rámci kompilace přes parametry
kompilátoru.
Použití je
velmi jednoduché, efektivní a flexibilní.
Když tedy
člověk vidí diskuse nad tématy „Closures“ (http://krecan.blogspot.com/2006/09/closures-v-jave-dkuji-nechci.html)
popr. jinými nápady co dát do nové verze Javy, nedá
mi to, než si nevzpomenout na triviální funkčnost
podmíněné kompilace popř. includů, která byla už v
Pascalu, neoplývajícím preprocesorem, a to již v
dobách kdy kraloval MS-DOS a dost možná i dříve za
dob prapradědečka CP/M.
Je
možno namítnout, že tehdy se ještě programovalo
procedurálně – můj názor ale je, že to s potřebností
a použitelností podmíněné kompilace (popř. includů,
kde je ovšem problém složitější) nesouvisí.
Implementace do kompilátoru by tedy jistě nebyla
technickým problémem.
Relevantnější námitkou proti potřebě podmíněných
kompilací je možná fakt, že stejně na úrovni
binárních souborů verzí neubude – to je pravda,
nicméně i jednotné snadno univerzalizovatelné
zdrojové kódy jsou velkou výhodou, zvláště dneska v
době open source.
Paradoxně
podle některých zdrojů důvodem k nezavedení direktiv
kompilátoru pro podmíněný překlad byla obava, ze by
to vedlo ke kódu, ktery bÿ byl specifický pro
platformy a tudíž odporoval filozofii jazyka „write
once, run anywhere“… Pokud to mělo význam v
počátcích, dnes, kdy je verzí Javy čím dále tím více
(nehledě na různé edice) tento argument podle mého
názoru smysl postrádá.
Osobně mne
problém s nutností udržovat knihovny, popř. sledovat
jaká verze cizích knihovnen se hodí pro různé verze
cílových JVM v kombinaci s jinými knihovnami, docela
trápí. Nejde totiž jen o sledování čísel verzí – s
novou verzí knihovny, obsahující změny ve funkčnosti,
téměř vždy dojde i k nekompatibilitě v jejím použití
(nemluvě o nekompatibilitě konfigurací), což znamená
při přenosu projektu na jinou verzi JVM reagovat na
přenos i na místech, kde se knihovna používá – a tím
se náš vlastní produkt stává nekompatibilní s verzí
starou. Možnost podmíněného překladu by usnadnila
vytváření knihoven kompatibilních se starou
funkčností ale nad novými platformami.
Máte někdo
pro tyto problémy nějaké jiné řešení, o kterém nevím,
není obecně zmiňováno resp. myslíte si, že je
nejlepší?
Diskuse na téma podmíněných kompilací v Jave se táhne
už mnoho let a na úrovni jazyka se jak vidno pořád
nic neobjevilo…
Jsem v
této oblasti exot? Také Vám podmíněná kompilace –
např. výše zmíněného „pascalského typu“ – v záplavě
nových a nových exotických a stále exotičtějších a
méně a méně pochopitelných navrhovaných features pro
Javu schází? Napadají Vás ještě jiné nástroje, které
by bylo možno na úrovni jazyka či jinde zavést, aby
se zvětšily a zjednodušily možnosti psaní
univerzálního „write once – run anywhere (tedy i nad
různými verzemi JDK nebo jiných potřebných knihoven)“
kódu?
P.S. Pokud jste před časem viděli první – nehotovou
– verzi tohoto článku, omlouvám se spolu s redakcí,
že došlo k jejímu předčasnému uveřejnění. Stalo se
tak omylem, se kterým sem neměl nic společného.