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.