Generovaný kód je zlo. A to i ve webových službách.

Minulý týden jsem zase trochu programoval, psal jsem jednu webovou službu. Ta měla WSDL definované třetí stranou, takže to byla poměrně jednoduchá a rutinní záležitost. Vzal jsem XML schema, z něj vygeneroval XmlBeans a začal jsem implementovat. Narazil jsem ale na jeden problém, který se mi nedařilo vyřešit.

Měl jsem vyrobit čtyři webové služby, jejichž odpovědi se lišily jen v pár detailech. V zásadě vypadaly takto:

<XXXResponse>
        <Version>1</m:Version>
        <RequestId>XYZ123</RequestId>
        <Result>SUCESS</Result>
        <XXXTransactionId>ABC456</XXXTransactionId>
</XXXResponse>

Místo XXX si představte různé fáze transakce. Můj problém spočíval v tom, že jsem nechtěl mít v programu čtyřikrát ten samý kus kódu pro generování odpovědi, chtěl jsem ho tam mít jen jednou. Jenže se to nedařilo, zkoušel jsem všelijaké finty s dědičností, vymýšlel jsem sofistikované wrappery, ale pořád jsem měl čtyři docela ošklivé bloky, které si byly ohromě podobné. Jediný rozdíl byl v jménu odpovědi a jménu transactionId. Ale tyto drobné rozdíly mi bránily v tom, udělat znovupoužitelný kód pro všechny webové služby.

Až mě napadla jednoduchá věc. Vždy jsem tvrdil, že generování kódu je zlo, kterému je potřeba se za každou cenu vyhnout. Ale u webových služeb jsem to tak nějak bral za dané. Znáte to, contract first přeci spočívá v tom, že začnu s návrhem XSD a podle něj píši kód. Je jen přirozené si ten kód pak nechat vygenerovat. A to je právě ta chyba. XML a Java jsou různé světy. Ano jsou si docela podobné, ale každý má jiný účel, jiné zvyklosti a jiné potřeby. Problém je, že, generovaný kód vždy odráží svůj původ.

Když generuji kód na základě XML, dostanu anemický model se kterým se mi pak špatně pracuje. Často pak skončím u toho, že mapuji vygenerované třídy na nějaké svoje vymazlené třídy a pak zase zpátky.

Když na druhou stranu generuji XSD na základě Javy, riskuji, že se s výsledným XML bude mým klientům špatně pracovat, že bude špatně rozšířitelné a podobně.

Je to podobný problém jako u objektově relačního mapování. Objekty a tabulky jsou si hrozně podobné, ale přesto je mezi nimi obrovská propast. Pamatuji si doby, kdy jsem generoval Javovské třídy na základě tabulek. Výsledek byl vždy žalostný. V poslední době jednoduše namapuji ručně psanou Javu na ručně optimalizovanou databázi a obě strany jsou spokojené.

Proč něco podobného nejde u XML? Pokud jste byli na mém lightning talku, možná si vzpomenete jak jsem si stěžoval, že v roce 2010 musím ručně mapovat XML třídy na normální třídy a zpátky. Mýlil jsem se, nemusím. Mohu udělat to samé jako s databází. Díky JAXB 2 mohu jednoduše namapovat svoje třídy na XML. Samozřejmě, v první iteraci si mohu pro jednoduchost ty třídy vygenerovat, ale pak už je ručně upravím a nikdy je negeneruji znovu.

A přesně tak jsem vyřešil svůj problém s duplicitou kódu. Ručně jsem si upravil JAXB třídy. Nejdřív jsem si vyrobil abstraktního předka všech odpovědí. To by ještě šlo zařídit úpravou XML schematu. Ale pak jsem do toho abstraktního předka přidal abstraktní metodu setTransactionId a dokonce jsem ho nechal implementovat nějaké rozhraní. To jsou koncepty, které v XML vůbec nemají smysl! Tím pádem ani nemá smysl pomýšlet na jejich generování.

Tím, že jsem udělal JAXB třídy nezávislé na XSD, jsem si zajistil, že je nemusím kopírovat do jiných interních tříd. Mohu svůj business kód udělat závislý jen na rozhraních, které XML třídy implementují. Konkrétně u mě to ušetřilo desítky řádků ošklivého kódu.

Kdybych měl dost odvahy, mohly bych dokonce i k těmto třídám přidat JPA anotace a ukládat je bez obav do databáze. Ale tak silný žaludek zatím ještě nemám.

Samozřejmě i tento přístup má své nevýhody. Asi největší je, že se mi může Java odchýlit od XML schematu. Může se mi stát, že udělám chybu, a začnu generovat nevalidní XML. Od toho ale máme testování, můžete použít třeba moji úžasnou knihovnu, která vám to pohlídá.

Zkuste se nad tím zamyslet. Generovaný kód nefungoval nikdy, vždy s ním byly jen problémy. Ale u webových služeb jsem ho alespoň já bral jako hotovou věc. Když se ho ale zbavíme, ušetříme si spoustu starostí. Náš kód může vypadat o něco víc objektově a my si užijeme mnohem víc legrace s programováním užitečných věcí.

10 Responses to “Generovaný kód je zlo. A to i ve webových službách.”

  1. ARny Says:

    dovolim si nesuhlasit. Zgenerovanie proxy tried z WSDL je ovela lepsie ako si ich napisat. Si predstavte, ze vam niekto zmeni ten model a znovu a znovu ho budete musiet prepisovat namiesto zgenerovania. To stoji vela casu a penazi a podla mna je to zbytocne.

    Tiez nesuhlasim s vetou: "Generovaný kód nefungoval nikdy, vždy s ním byly jen problémy.". Vela generovaneho kodu funguje perfektne a pokial je to generovanie urobene dobre tak setri cas aj peniaze (nehovorim o nervoch).

  2. Lukáš Křečan Says:

    Potíž s generovaným kódem je ten, že si člověk myslí, jak moc mu šetří práci a často to tak není. Řekněme, že mám vygenerovanou proxy třídu z WSDL. Stejně někde musím ručně naplnit data, která se mají poslat. Pokud mi někdo změní rozhraní, stejně musím ručně změnit to plnění dat takže si moc práce neušetřím. Naopak, tím, že generovaný kód obvykle obsahuje hromadu duplikací, musím tu změnu udělat na několika místech.
    Já mám s generovaným kódem jen negativní zkušenosti, ale je možné, že jsem výjimka. Jen upozorňuji, že nemluvím o kódu generovaném za běhu, ten je často užitečný. Mluvím o generování zdrojového kódu, který jen někde leží a já na něj nesmím ani šáhnout.

  3. v6ak Says:

    Generování toString(), equals(Object) a hashCode() díky anotacím je taky zlo?

  4. Lukáš Křečan Says:

    @v6ak: To je zajímavá otázka. Pokud je to generováno za běhu, tak ne. Generování za běhu je věc užitečná, psal jsem je o generování zdrojového kódu. Pokud je generovaný přímo zdrojový kód, tak je to složitější. Já to taky používám, ale jsou s tím problémy. Například na to pak nejsou unit testy. Dále pak, když přidám nějakou položku, hashCode, equals ani toString se mi nezmění. Takže pro mě je to zlo, ale přijatelné 🙂

  5. v6ak Says:

    Čím je to dáno? Je to opravdu tím, zda je to za běhu? U zmíněných věcí mám namysli generování během kompilace: http://projectlombok.org/ .

    Mám teda i zkušenosti s Google Protocol Buffers, které sice udělaly svoji práci, ale musel jsem to obalovat vlastní vrstvou, mj. kvůli polymorfismu.

    Generování je zlo, pokud:
    * je generovaný kód míchán s ručně udržovaným (typicky v jednom souboru), nebo
    * je do generovaného kódu ručně zasaženo (nepřál bych si dne, kdy je potřeba jej přegenerovat...)
    * logika generovaného kódu ne úplně sedí (někdy lze vyřešit další vrstvou) - o tom je v článku a i moje poznámka o protobuf

  6. lzap Says:

    Zas tak bych to nepaušalizoval. Ne vždy je možné si "diktovat" jak bude interface vypadat. Takže se prostě někdy musí generovat...

  7. Vlastik Eliáš Says:

    Příme mapování XML JavaBeans DB se společnou sadou JavaBeans jsem dělal již někdy v roce 2004, tehdy s použitím frameworků Castor XML a Hibernate 2.1 (mapování popsáno v XML souborech). Částečně jsem ty JavaBeans a mapovací soubory generoval a pak doupravoval ručně. Použito bylo pro exporty a importy dat (XML soubory velké i několik MB).
    Stejné JavaBeans pak tvořili model pro web aplikaci, která data zobrazovala (tehdy ještě Struts), a pro částečné exporty dat do dalších různých formátů (XML i nonxml). Fungovalo/funguje to perfektně dodnes (viz http://kramerius.nkp.cz).
    S dnešními frameworky by to podle mě bylo ještě víc v pohodě, myslím že na to není třeba silný žaludek.

  8. Milan Fořt Says:

    Osobně se snažím nemíchat kód endpointů webových služeb s kódem dao komponent. Endpointy jsou vzdáleným rozhranním a tomu odpovídá datový model (často založený na podnikovém canonical data modelu). Snažím se tvořit hrubozrný interface. Byznys logiku do endpointů nedávám, od toho mám komponenty byznys služeb. Tam už se objevuje i jemnozrný interface, se kterým se dobře pracuje z javy. Perzistenci beru jako pomocnou službu, ať už řešenou aplikační databází nebo voláním vzdálených služeb příslušné byznys domény, vlastnící data. U endpointů třeba z Jax-WS je také problém v konfrontaci se Springem, měl jsem problémy třeba s anotacemi kolem aspektů - Spring tvořil proxy a narazil ... Kde s Tebou ale Lukáši plně souhlasím je pocit něčeho chybějícího mezi contract-first a contract-last přístupem u ws stacků jako je třeba Jax-WS. Stává se mi, že někde potřebuju pracovat s XML příchozí zprávy a v případě kompozice služeb kus XML efektivně poslat jiné službě. Převod XML-Object a pak zas Object-XML se mi nelíbí, ještě víc se mi nelíbí způsoby, jak se k XML dostat z Jax-WS. V dokumentově orientovaných zprávách, na službách s hrubozrným rozhranním je tohle běžné. Řešením může by mohlo být použití třeba Service Data Objects.

  9. Milan Fořt Says:

    Svůj názor ještě doplňuji o jednu situaci. Ať už jako poskytovatel nebo konzument webové služby pracuji s dokumentově orientovaným rozhranním. Použité XSD obsahuje byznysově zajímavé věci a vzniká jako výstup designera v rámci MDA, generuje se z UML. Nicméně používám architekturu, která do zpráv přidává aspekty jako je zabezpečení (autorizace - ACL záznamy u entity ..), audit informace/changelog apd. Tady si už nevystačím s hlavičkou zprávy, potřebuju obohatit zprávy o tyto informace. Technicky na to lze myslet pomocí xsd:anyType a JAX-WS se s tím dokáže nějak vyrovnat. Ale právě tady je zakopaný pes, JAXB objekty mě sice odstiňují, ale jen napůl. Stejně si pak musím XML navíc zpracovat sám. JAXB binding mě nepomáhá, zachraňuji to až Dozerem na úrovni mapování mezi java objekty. Ve chvíli, kdy implementace služby provolává jiné služby a dává jim podobné XML je vám asi jasné, kolik času mi trvá tvorba skutečné byznys logiky a kolik času propálím na uvedených věcech.

  10. ARny Says:

    Suhlasim s v6ak, ze do generovaneho kodu nesmie byt zasahovane rucne.
    Vidim ze p. Krecan je skor zastanca online generovania kodu na pozadi ala hibernate/JPA atd. My ked sme s JPA robili prisli sme na problemy vyplivajuce objektoveho mapovania relacnych DB. Na niektore situacie reagoval ten vygenerovany kod nie tak ako by sme chceli a preto sme boli donuteni prejst na iBatis a pisat si vsetky mapovania sami. Jeho obrovska vyhoda je ze mate vsetko pod kontrolou a tak sme u neho zostali. Jeho problem vsak je ze ktorukolvek zmenu musite pracne upravovat v XML a entitach mapovania co vyzaduje cas a vznika tam velke mnozstvo chyb. Podla mna je vyhodne zgenerovat tieto casti kodu kedze nieje potrebne aby ich programator pisal rucne. Je to efektivne a prakticky nevznikaju chyby. Samozrejme je potrebne oddelit generovany kod a casti kodu ktore su napisane rucne (napr vlastne selecty). Ja osobne som zastanaca generovania zdrojoveho kodu a odporca roznych online generatorov. Proste som rad ked mam vsetko pod kontrolou ale zakladne veci nemusim pisat.
    Samozrejme to aku technologiu ci framework pouzijem zavisi na tom co idem robit pripadne co si ziada zakaznik ale inak rad pouzivam princip generuj to jednoduche a casto sa vyskytujuce a ostatne si napisem sam.