Modularita 2: Taxonomie

Druhý díl mých poznámek na téma modularita. Přečíst nejdřív první díl o ekonomických motivech modularity není úplně nutné, ale doporučeníhodné.

Také rozmanitost technologických druhů modularity může být překvapivá, protože si o ní často vytvoříme intuitivní představu na základě výchozího toolchainu jednoho konkrétního jazyka nebo ekosystému a další možnosti už cíleně nezkoumáme. Až opravdoví polygloti s překvapením zjišťují, kolik může mít modularita podob a z toho odvozených vlastností. Brutálně zjednodušený model modularity Mavenu a Javy budiž exemplárním příkladem.

Dospěl jsem k přesvědčení (netuším, zda je to takhle někde formalizováno), že nejužitečnějším základem taxonomie technických řešení modularity jsou opět kontrakty, konkrétně způsob jejich ověřování a vynucování. Kontrakt mi v dalším textu trochu zdegeneroval jen do podoby datových typů, ale myšlenky jsou platné obecně. Třeba základní intuici můžeme opět získat z ekonomické praxe reálného světa, kde kontrakt má podobu smlouvy:

  • Při jejím podpisu každá ze zúčastněných stran ověří, že je schopná dostát závazkům ve smlouvě popsaným a podpis je potvrzením tohoto faktu. Tuto úlohu má v programování klasicky kompilace.
  • Při faktickém plnění dodávky popsané smlouvou je vhodné ověřit, zda má dodávka parametry očekávané odběratelem. Ne kvůli zlému úmyslu dodavatele, ale protože od papíru smlouvy k finálnímu výrobku je hóóódně dlouhá cesta a odchylky jsou spíš pravidlem, než vyjimkou. A to neuvažujeme volnější varianty smluvního vztahu jako rámcové smlouvy, kolektivní smlouvy apod. Tuto úlohu má v programování linking, asserty a/nebo integrační testy.

V drobnějších detailech už ale tohle srovnání není tak přímočaré, takže je potřeba to trochu rozvést. Vezmu to primárně z pohledu Javy, protože tam znám zoo modulárních technologií nejlépe.

Začnu od této varianty, protože to je to, co si většina Javistů odkojených Mavenem pod modularitou představí. Tato varianta stojí na představě modulů jako lego kostek - univerzálních hotových (zkompilovaných) bloků, které si vybíráme z krabice (repositáře) a podle potřeby jen skládáme do výsledné stavby. To se v Mavenu skvěle osvědčilo v případě standardních knihoven, ale dále je to tím problémovější, čím více knihovny připomínají plnohodnotné moduly (tzn. nejsou zcela zapouzdřené, ale naopak mají volné tranzitivní závislosti).

Je to proto, že Java nedokáže prakticky nijak pracovat s kontrakty v bajtkódu:

  • Java zná pouze dynamický linking a late binding. Co se při startu aplikace objeví na classpath, to v aplikaci bude. Java tím své programátory sice chrání před složitostí různých linkovacích konfigurací, známých ze systémových jazyků, ale zároveň jim tím z vývojových nástrojů bere přesně ty úrovně abstrakce, na kterých by se modularita dala řešit ve všech svých podobách nejpřirozeněji. Nabízí místo toho vlastní koncept runtime classloaderů, kde je sice teoreticky možné nasimulovat téměř jakýkoli linkovací model, ale…
  • V Javě neexistuje systémová možnost validovat mezi sebou kontrakty modulů načtených z bajtkódu. Jinými slovy jediný okamžik, kdy se fakticky kontroluje kompatibilita kontraktů použitých modulů je už při jejich kompilaci a jen vůči kompilovanému kódu. Jakákoli změna od té doby představuje neodhalitelnou našlápnou minu v runtime. To činí kanonický problém modularity - diamond problem - nedetekovatelný a tudíž neřešitelný.

Podtrženo sečteno v Javě si nikdo nemůže být moc jistý finálním složením modulů při spuštění (to je přesně ta dlouhá cesta od podpisu smlouvy k faktické dodávce) a uživatele modulu na případnou nekompatibilitu nic neupozorní dokud mu za běhu nezlomí vaz. Ups…

Je ale Java opravdu vadná “by design”? Opravdu by stačilo přidat buď rozšířené možnosti linkeru (třeba jako má jazyk C) nebo možnost explicitní validace bytekódu, aby se stal modulární vývoj jednoduchý a spolehlivý? ‎Ano i ne. Pochopit neurčitost této odpovědi znamená pochopit fundamentální technickou obtížnost modularity a nejlíp se to chápe, když se to v praxi zkusí.

VerifaLabs to the rescue

Validací bajtkódových kontraktů se zabývá spin-off VerifaLabs z mé drahé Alma mater ZČU. A mají fantastické výsledky, které jsem měl možnost vyzkoušet díky starým kontaktům zdarma. Jejich produkt se dá mimo jiné použít zcela jednoduše jako Maven plugin, který ve fázi test (extrémně zjednodušeně řečeno) zkontroluje za pomocí černé magie každý bajtkódový invoke a accessor proti jeho implementaci reálně přístupné na classpath. To je přesně to, co jsem výše psal, že v Javě chybí.

V praxi ale u jakéhokoli trochu rozsáhlejšího reálného projektu vypadnou z prvního běhu takové validace desítky až stovky tisíc nalezených nesrovnalostí. Znamená to, že jsou v projektech stovky tisíc chyb, které jen čekají na příležitost? Ne, naprostá většina nálezů budou falešně pozitivní hlášení způsobené tím, že z modulů se používá pouze podmnožina jejich funkčnosti (např. pouze jedna z X podporovaných databází). Validace pak může selhat protože:

  • Na classpath chybí závislost, která tam byla v době kompilování použitého modulu. To nemusí být špatně, pokud se daná závislost používá pouze v kontextu nevyužité funkčnosti.
  • Na classpath je přítomná jiná verze závislosti, než která tam byla v době kompilace a která je nekompatibilní. To nemusí být špatně, protože závislost tam může být kvůli použití v jiném modulu a nekompatibilní prvek je volán opět jen v kontextu nevyužité funkčnosti (falešný diamond problem).

Obé je důsledek toho, že živé code path jsou určovány daty a ty v době sestavení prostě nemáme (nemluvě o undecidable problémech). To je důvod, proč se validace kontraktů v link time modularitě nedá dělat plně automaticky a máme prakticky pouze následující možnosti:

  • Zpětně asistovat expertním odhadem automatickému validátoru v určování falešně pozitivních hlášení (VerifaLabs)
  • Dopředně expertním odhadem sestavit separátní strom runtime závislostí, aby se nekontrolovalo všechno se vším (tak trochu OSGi, ale dá se k tomu znásilnit i Maven).

Druhá možnost mimochodem ilustruje ještě jeden důvod, proč nelze strom runtime závislostí odvozovat automaticky z kompilačních stromů jednotlivých modulů. Kromě toho, že nelze zjistit, jaké funkčnosti budou z modulů využity, je tu ještě ona technika dependency inversion. Zatímco v runtime závislostech logicky závisí vždy odběratel na dodavateli, v kompilačních závislostech je směr závislosti arbitrární volba poplatná ekonomické konstrukci okolo vyvíjeného produktu, jak jsem psal v předchozím díle. Tu počítač znát nemůže.

Závěrem tedy můžeme konstatovat, že ano, existují možnosti, kterými můžeme zvýšit kvalitu modulárního produktu. Zároveň je ale vidět, že jednak nemohou být plně automatické (takže ani zadarmo) a jednak se bude stále jednat vždy “jen” o expertní odhad a jejich přínos proto bude přímo závislý na kvalitě a lenosti oněch expertů. A víte, jak to mají programátoři s leností.

Co jsem chtěl touhle kapitolou říct je to, že modularita založená na binárních blocích nemůže nikdy vypadat jako skládání lego kostiček, ale spíš jako transplantace orgánů. Několika najednou. Bez špičkových chirurgů je prognóza přežití malá a to je přesný opak toho, o co se každý inženýrský obor musí snažit.

Compile time modularita

Když se na problémy předchozí kapitoly podíváme tou intuicí odvozenou z ekonomické praxe, kterou jsem popsal v úvodu, jsou ve skutečnosti banálně samozřejmé. Snažíme se totiž rozjet výrobu vlastního produktu se subdodavateli podle smluv, které podepsali někdy jindy s někým jiným. V praxi je naopak pro každý větší projekt samozřejmé, že se se všemi subdodavateli podepíše separátní smlouva i kdyby měla být úplně stejná, jako mají ti subdodavatelé na jiných projektech.

Tomu by podle paralel načrtnutých dříve odpovídalo v programování kompilovat v projektech všechny závislosti přímo ze zdrojáků specificky pro konkrétní projekt. To se na první pohled může zdát (Javistům a spol.) jako blbost, ale v praxi se to skutečně provozuje. Kromě obligátního příkladu balíčkovacího systému Gentoo je to hlavně programátorům bližší příklad buildovacího systému Haskellu - Cabal. Haskell totiž dostal plnohodnotný systém modularity binárních modulů až nedávno a proto jeho komunita raději zvolila právě strategii buildování balíků že zdrojáků. Má to proti binární modularitě následující výhody:

  • Kompilátor prostě je a bude nejlepší místo pro vzájemnou validaci kontraktů.
  • Můžeme konfigurovat už přímo kompilaci zavislostí tak, aby přesně odpovídala potřebám projektu. Nemusejí pak vzniknout žádné “volné konce” jako v případě binárních modulů.

Nejsem Haskell expert, abych kvalifikovaně zhodnotil pro a proti této strategie, četl jsem, že v praxi má negativ také dost, ‎ale dokážu si představit, že v ní některé problémy link time modularity prostě nejsou. A že je to tedy dobrý kompromis jak dosáhnout na výhody modularity bez přílišných obtíží.

Run time modularita

Pak je tu třetí druh modularity. Takový, kde se moduly přidávají a vyměňují za běhu aplikace. Ať už jste uvěřili marketingovým materiálům OSGi kontejnerů nebo kázání microservice evangelizátorů, nemohu vám nabídnout nic než svojí soustrast. Budete řešit všechny problémy popsané až doted a ještě nějaké navíc a to v reálném čase s pomocí mnohem slabších nástrojů.

Závěrem

Celým článkem se táhne přirovnávání informačních systémů k ekonomickým situacím v reálném světě jako takové rozšíření Conwayova zákona. Nemám tušení, jestli tuhle filosofii někdo rozpracoval hlouběji ani jestli to je vůbec korektní směr myšlení. Sepsal jsem to proto, že mi to pomohlo pochopit jevy, které čistou computer science nezajímají nebo je nedokáže vysvětlit. ‎Pochopil jsem, že v praxi se musí stejná pozornost, která se věnuje slovu informační, věnovat i‎ slovu systémy. ‎Tohle pro mě tedy není konec, ale začátek. Jestli máte někdo doporučení kam dál, sem s ním.