Přestože mnozí věří, že programování je exaktní věda, ve skutečnosti má blíž k literatuře. Program se nedá vypočítat, program se musí napsat. Programátor musí, stejně jako spisovatel, psát jazykem kterému rozumí ostatní programátoři a dodržovat určitá stylistická pravidla. Počítačové jazyky totiž existují kvůli lidem a ne kvůli počítačům, ty by se spokojily s nulami a jedničkami. Větší pozornost je sice nutné věnovat kritickým (napínavým) úsekům, ale pokud autor ztratí ze zřetele soudržnost celku, výsledkem bude škvár, který sice možná bude sdělovat příběh, ale prokousat se jím bude peklo.
Na rozdíl od spisovatelů jsou však programátoři nuceni pracovat v týmech. Mají tak problémy, kteří spisovatelé vůbec neznají. Některé jsou technického rázu a jdou celkem uspokojivě vyřešit, např. jak spojovat kusy napsané různými autory do jednoho celku (VCS). Horší vznikají tím, že programátoři jsou lidi. Lidská interakce je nesmírně složitá a ke strojové přesnosti a neutrálnosti má na hony daleko. Představte si to, kvůli mezilidským vztahům dokonce vzniklo několik vědních oborů (sociologie, psychologie, psychiatrie, politologie, ekonomie atd.)! Manažeři programátorů se navíc často domnívají, že se pohybují ve výhradně technické oblasti, a tak mohou problémy druhého typu v klidu nepovšimnuty bujet a stávat se patologickými. Ale to je jiná kapitola.
Dnes se chci věnovat jedné vlastnosti, kterou mají programátoři se spisovateli společnou. Každý autor, ať už píše cokoliv, se se svým dílkem ztotožňuje. Je to naprosto přirozené: Trávíte s ním spoustu času, stojí vás mnoho úsilí a chcete aby se mu dařilo co nejlépe. Když pak přijde kritika, ať už oprávněná nebo ne, je těžké ji přijmout s chladnou hlavou. U spisovatelů to vede k úsměvným historkám ve společenských časopisech, u programátorů k frustraci a nepřátelství. A protože se programátoři potkávají v práci každý den, je to pro ně daleko horší problém než pro spisovatele. Pak je ale těžké napsat dobrý program.
Jak tedy programovat bez ega? V první řadě musí člověk přijmout fakt, že se může mýlit. Vždy se najde někdo, kdo o daném problému ví víc než vy. Zvlášť když strávíte se svým produktem delší dobu, je těžké najít odstup a snadno pak přehlédnete i očividné chyby. Pravdu naopak mohou mít i lidé, od nichž by to člověk vůbec nečekal. A tak nezbývá než věnovat pozornost každé výtce, ať už pochází z jakéhokoliv zdroje.
A opačně, pokud kritizujeme, snažme se nedotknout se osobnosti autora kódu. Kritizujte kód, ne jeho autora. Snažte se navrhovat, jakým způsobem lze kód vylepšit. Pokud je to možné, opřete se o fakta (standardy, měření, literatura, články na webu, dohodnuté konvence atd.). Destruktivní kritika typu "seš snad úplně blbej" nebo "všechno stojí za hovno" nikam nevede, jen šíří pocity zoufalství. Zkuste odhadnout, kolik toho ten druhý "skousne" a buďte hodní na kolegy, kteří nemají takové sebevědomí. To, že má někdo méně zkušeností nebo znalostí neznamená, že je horší než vy. Navíc za rok může být situace klidně opačná. Pokud tým funguje, jeho členové jsou schopni se navzájem učit a mají ze spolupráce dobrý pocit.
Aby se programátoři nestyděli zeptat se kolegy o radu, musí v týmu panovat přesvědčení, že mýlit se je lidské a že víc hlav víc ví. Každý má občas špatný den a jediné co se s tím dá dělat, je umožnit, aby se na chyby přišlo co nejdříve. Dobrou pomůckou mohou být techniky z extrémního programování, jako párové programování, code review, kolektivní vlastnictví kódu a stand-up meetingy (také známé jako scrum). Mám ale obavu, že prvotní je dobrý výběr lidí; nehledět při náboru pouze na technické znalosti, ale i na to, zda je přijímaný člověk týmový hráč. Pokud máte tým složený ze samých "hyper-senzitivních code-machines", žádné metodologie vám asi nepomůžou.
Já sám osobně mám pocit, že už dokážu obstojně snášet kritiku kódu, který jsem napsal. Kritizovat kód kolegů mně ale stále nejde tak, jak bych si představoval. Je těžké vybrat správná slova, aby druhý pochopil, že ho nechci urazit ani snížit jeho zásluhy. Někdy mě zradí jazyk, někdy se moc rozohním, někdy toho mám prostě dost a někdy zafunguje ponorková nemoc, protože pracuju ve firmě, kde jsou trvalé týmy - roky sedíte v kanceláři se stejnými lidmi. Tímto se proto omlouvám každému, koho jsem kdy mohl urazit a chci aby jste věděli, že kašlu na to kdo je autorem, zajímá mě pouze kód a aby byl dobrý.
Ještě poznámka na závěr: Zajímavá je souvislost s jiným principem efektivního programování: Ukazuje se totiž, že programovat bez ega znamená napsat pouze tolik, kolik situace vyžaduje - ne méně a ne více. Nemá prostě cenu vymýšlet všechny varianty, které by po nás mohl uživatel teoreticky chtít, a potom napsat nějaké úžasně univerzální über-řešení; lepší je napsat program, který řeší pouze aktuální problém, a pokud bude potřeba, bude se průběžně doplňovat a zesložiťovat. Je to staré pravidlo YAGNI (You Ain't Gonna Need It), které jinými slovy říká, že i když jste bůhvíjak dobří programátoři, nesmíte se bát obyčejných jednoduchých řešení - není to ostuda.
Egoless Programming
Ten Commandments of Egoless Programming
čtvrtek, srpna 28, 2008
sobota, července 26, 2008
Ornament a kód
Ornament je zločin, vytanula mi na mysl slova Adolfa Loose, když jsem brousil starou olouhovanou skříň. Má totiž několik ozdobných vlnek, které bylo potřeba pracně ručně obrušovat každou zvlášť. A pak mě napadlo, že to vlastně platí víc v programování než v architektuře.
Nejdřív mi dovolte stručné vysvětlení úvodního citátu (čtenáři s humanitním vzděláním laskavě prominou):
Moderní architekti prohlásili dům za stroj na bydlení (*1). Architektura podle nich není umění, ale věda o bydlení, která má upravit hmotné prostředí potřebám lidského živočicha. Nenávist k ornamentu je logickým důsledkem tohoto názoru - vždyť k čemu jsou lidem propracované ozdobičky, když vše co potřebují je čerstvý vzduch, dostatek slunce, teplo a bydlení v obytné zóně bez továren? "Less is more" (Mies van der Rohe) se stalo heslem několika generací architektů. Le Corbusier řekl: „Dekorace je smyslné a primitivní povahy, stejně jako barva, a hodí se toliko pro nižší třídy, sedláky a divochy.“

copyright Jeffery Howe
Adolf Loos i Le Corbusier sice zapomněli že lidé v domech nejen bydlí, ale také chodí kolem, přesto však udělali se svým názorem díru do světa. Podařilo se jim přesvědčit ostatní o své pravdě a nadlouho ovlivnili myšlení architektů a urbanistů na celém světě. Důsledky známe i u nás v podobě uniformních panelových sídlišť, které přímo vycházejí z jejich ideálů - i když s jejich provedením by asi Le Corbusier nesouhlasil. Konec dominance funkcionalistické architektury znamenal až nástup postmoderny v šedesátých letech a "less is more" nahradil Robert Venturi heslem "less is bore".

copyright tato grasso
A jak to souvisí s programováním? Každý, kdo někdy četl cizí kód, si hned všimne nezvyklých konstrukcí a způsobu formátování kódu. Někdy zaujme precizní tabulární formátování, někdy tvrdošíjná maďarská notace, jindy zas poznámky ve kterých vede programátor dialog sám se sebou. To vše odvádí pozornost čtenáře od funkce k formě. Nejsou to snad ornamenty?

Zdálo by se že tu ornamenty hájím, protože vnášejí krásu do všednosti, vítané potěšení znavenému duchu. Jenže mezi programováním a architekturou je jeden podstatný rozdíl. Architekt musí počítat s tím, že kolem jeho děl budou chodit i laici, kteří o zločinnosti ornamentu nemají nejmenšího tušení. Architekt by měl prostředí ušít na míru všem, nejen kolegům z branže a odborné kritice. Vhodně zvolený ornament pak může sloužit jako vodítko i (z architektova hlediska) nevzdělanému tupci, který by jinak jeho střízlivý obytný dům mohl považovat za vězení.

Kolem vašeho kódu ale laici chodit nebudou. Váš kód se dostane do ruky úzké skupině lidí s (někdy až příliš) specializovaným výcvikem, kteří budou mít za úkol se v něm zorientovat a provést potřebné změny. Pro ně je jakýkoliv ornament zdržením. Nejčitelnější kód je takový, který se striktně drží stanovených konvencí. Kód by měl vypadat tak, jako kdyby ho napsal jeden programátor během jednoho sezení.

Ornament v kódu je jakékoliv porušení stanovených konvencí. Napíšete-li kus kódu jinak, než je ve vašem týmu obvyklé, nutíte kolegy aby při čtení této části kódu přemýšleli ne o tom, co kód dělá, ale o jeho formátování - proč je zapsán tímto neobvyklým způsobem? Je za tím nějaká myšlenka, která mi uniká? Něco na co chtěl autor upozornit? Bohužel, většinou se jedná jen o neznalost konvencí, okamžitý nápad jak kód napsat "lépe" (v lepším případě), nebo o přesvědčení autora že jeho konvence jsou lepší a ostatní by je měli přijmout.

Jak mají vypadat konvence psaní kódu je sice sporná věc, ale jakmile se na nějakých shodnete, je nutné je dodržovat. Osobně preferuju držet se výchozích nastavení Visual Studia a ReSharperu. Proč pracně přenastavovat něco, co používají tisíce programátorů na celém světě? Tím odpadne mnoho diskusí ohledně formátování závorek a mezer kolem operátorů. Mnoho dalších věcí za vás nástroje nevyřeší (jmenné konvence, pořadí prvků atd.), ale na internetu je spoustu zdrojů a vzorových "code conventions" dokumentů i pro váš programovací jazyk.

Kód odpovídá strukturálním a technickým stránkám budovy. Je to nosný systém, vodotěsné zastřešení, rozvody energie a vody, výtahy. Pokud chcete postavit dobrý dům, je bezpodmínečně nutné udělat tyto základní věci dobře a umožnit údržbářům snadný přístup ke všem důležitým bodům. Na stavbě si nemůžete vymyslet svůj vlastní a zcela nový systém elektrických rozvodů a pojistek, nedělejte to ani v kódu. Na Grand Prix Obce architektů to sice nestačí, ale bez dobrého základu nepostavíte ani kůlnu. Dodržování konvencí zdaleka není všechno, ale usnadní život vašim kolegům (a také vám, až se budete za rok divit proč jste to tenkrát takhle napsali). Nechte si ornamenty do uživatelského rozhraní, ve stylistické úpravě kódu platí "bore is more".
*1 Le Corbusier ([1923] 1952): Towards a New Architecture. London
*2 http://cs.wikipedia.org/wiki/Funkcionalismus
UPDATED: 2008-07-26 Přidány citáty Mies van der Rohe a Roberta Venturiho.
Nejdřív mi dovolte stručné vysvětlení úvodního citátu (čtenáři s humanitním vzděláním laskavě prominou):
Moderní architekti prohlásili dům za stroj na bydlení (*1). Architektura podle nich není umění, ale věda o bydlení, která má upravit hmotné prostředí potřebám lidského živočicha. Nenávist k ornamentu je logickým důsledkem tohoto názoru - vždyť k čemu jsou lidem propracované ozdobičky, když vše co potřebují je čerstvý vzduch, dostatek slunce, teplo a bydlení v obytné zóně bez továren? "Less is more" (Mies van der Rohe) se stalo heslem několika generací architektů. Le Corbusier řekl: „Dekorace je smyslné a primitivní povahy, stejně jako barva, a hodí se toliko pro nižší třídy, sedláky a divochy.“

copyright Jeffery Howe
Adolf Loos i Le Corbusier sice zapomněli že lidé v domech nejen bydlí, ale také chodí kolem, přesto však udělali se svým názorem díru do světa. Podařilo se jim přesvědčit ostatní o své pravdě a nadlouho ovlivnili myšlení architektů a urbanistů na celém světě. Důsledky známe i u nás v podobě uniformních panelových sídlišť, které přímo vycházejí z jejich ideálů - i když s jejich provedením by asi Le Corbusier nesouhlasil. Konec dominance funkcionalistické architektury znamenal až nástup postmoderny v šedesátých letech a "less is more" nahradil Robert Venturi heslem "less is bore".
copyright tato grasso
A jak to souvisí s programováním? Každý, kdo někdy četl cizí kód, si hned všimne nezvyklých konstrukcí a způsobu formátování kódu. Někdy zaujme precizní tabulární formátování, někdy tvrdošíjná maďarská notace, jindy zas poznámky ve kterých vede programátor dialog sám se sebou. To vše odvádí pozornost čtenáře od funkce k formě. Nejsou to snad ornamenty?

Zdálo by se že tu ornamenty hájím, protože vnášejí krásu do všednosti, vítané potěšení znavenému duchu. Jenže mezi programováním a architekturou je jeden podstatný rozdíl. Architekt musí počítat s tím, že kolem jeho děl budou chodit i laici, kteří o zločinnosti ornamentu nemají nejmenšího tušení. Architekt by měl prostředí ušít na míru všem, nejen kolegům z branže a odborné kritice. Vhodně zvolený ornament pak může sloužit jako vodítko i (z architektova hlediska) nevzdělanému tupci, který by jinak jeho střízlivý obytný dům mohl považovat za vězení.

Kolem vašeho kódu ale laici chodit nebudou. Váš kód se dostane do ruky úzké skupině lidí s (někdy až příliš) specializovaným výcvikem, kteří budou mít za úkol se v něm zorientovat a provést potřebné změny. Pro ně je jakýkoliv ornament zdržením. Nejčitelnější kód je takový, který se striktně drží stanovených konvencí. Kód by měl vypadat tak, jako kdyby ho napsal jeden programátor během jednoho sezení.

Ornament v kódu je jakékoliv porušení stanovených konvencí. Napíšete-li kus kódu jinak, než je ve vašem týmu obvyklé, nutíte kolegy aby při čtení této části kódu přemýšleli ne o tom, co kód dělá, ale o jeho formátování - proč je zapsán tímto neobvyklým způsobem? Je za tím nějaká myšlenka, která mi uniká? Něco na co chtěl autor upozornit? Bohužel, většinou se jedná jen o neznalost konvencí, okamžitý nápad jak kód napsat "lépe" (v lepším případě), nebo o přesvědčení autora že jeho konvence jsou lepší a ostatní by je měli přijmout.

Jak mají vypadat konvence psaní kódu je sice sporná věc, ale jakmile se na nějakých shodnete, je nutné je dodržovat. Osobně preferuju držet se výchozích nastavení Visual Studia a ReSharperu. Proč pracně přenastavovat něco, co používají tisíce programátorů na celém světě? Tím odpadne mnoho diskusí ohledně formátování závorek a mezer kolem operátorů. Mnoho dalších věcí za vás nástroje nevyřeší (jmenné konvence, pořadí prvků atd.), ale na internetu je spoustu zdrojů a vzorových "code conventions" dokumentů i pro váš programovací jazyk.

Kód odpovídá strukturálním a technickým stránkám budovy. Je to nosný systém, vodotěsné zastřešení, rozvody energie a vody, výtahy. Pokud chcete postavit dobrý dům, je bezpodmínečně nutné udělat tyto základní věci dobře a umožnit údržbářům snadný přístup ke všem důležitým bodům. Na stavbě si nemůžete vymyslet svůj vlastní a zcela nový systém elektrických rozvodů a pojistek, nedělejte to ani v kódu. Na Grand Prix Obce architektů to sice nestačí, ale bez dobrého základu nepostavíte ani kůlnu. Dodržování konvencí zdaleka není všechno, ale usnadní život vašim kolegům (a také vám, až se budete za rok divit proč jste to tenkrát takhle napsali). Nechte si ornamenty do uživatelského rozhraní, ve stylistické úpravě kódu platí "bore is more".
*1 Le Corbusier ([1923] 1952): Towards a New Architecture. London
*2 http://cs.wikipedia.org/wiki/Funkcionalismus
UPDATED: 2008-07-26 Přidány citáty Mies van der Rohe a Roberta Venturiho.
středa, července 09, 2008
Přednáška o CruiseControl.Net a MSBuild
Dneska jsem dělal interní přednášku pro kolegy ve firmě o průběžné integraci s pomocí CruiseControl.Net a MSBuild, a o našem build systému postaveném na těchto technologiích. PowerPointovou prezentaci si díky laskavému svolení mého zaměstnavatele můžete stáhnout zde.
Jediné co nám chybí k dokonalému build systému je Big Visible Cruise nebo The Orb. Ale až budu mít trochu času, chtěl bych nějakou takovou hračku pořídit;)
Jediné co nám chybí k dokonalému build systému je Big Visible Cruise nebo The Orb. Ale až budu mít trochu času, chtěl bych nějakou takovou hračku pořídit;)
úterý, června 24, 2008
Jak udělat espresso
Protože mně Nikdo předal štafetu, tady je moje oblíbené video: Jak správně na espresso. Mimochodem, mají tam nádhernou mašinku. Jednou bych chtěl takovou doma:-)
Kafe které tam používají (Ethiopia Sidamo) se dá sehnat i u nás, např. já ho kupuju v Mama Coffee kde ho mají v super kvalitě a za rozumnou cenu.
Kafe které tam používají (Ethiopia Sidamo) se dá sehnat i u nás, např. já ho kupuju v Mama Coffee kde ho mají v super kvalitě a za rozumnou cenu.
pátek, června 20, 2008
Když Já není Já
Nebojte, nebudu tu rozebírat schizofrenní poruchy vyvolané dlouhodobým zíráním do monitoru. Jde o klíčové slovo this v C# a o jeden zajímavý případ, kdy this není this, nebo alespoň není to this, které jsme si mysleli že bude.
Pro začátek si představte, že máte dva objekty, mezi kterými je obousměrná vazba 1-N. Například User a Task, tedy uživatel a jeho úkoly, kde úkol může být přidělen nejvýše jednomu uživateli:
Protože nepracujeme ve státním sektoru, Tasků je hodně a je nutné, aby si naše aplikace pamatovala úkoly uživatelů i zítra. Použijeme tedy nějakou databázi; tím myslím existující databázový systém, který si stáhneme z internetu (MS SQL Express, MySQL apod.). Nezačneme tedy implementací vlastního RDMS, ani nebudeme simulovat databázi pomocí mnoha XML souborů uložených kdesi na disku (asi se divíte proč se s tím tak rozepisuju, ale nevěřili by jste, kolik lidí nemá rádo databáze, co si sami nenapsali).
A protože jsme vychytralí, použijeme nějaký O/RM nástroj pro mapování objektů do databáze. A jelikož nejsme bohatí, bude to NHibernate, který je zdarma, a podporuje víc databází než LINQ to SQL (navíc jsme chudí takže stále pracujeme ve Visual Studiu 2005 kde není podpora lambda výrazů).
Když budeme tedy chtít přiřadit Task nějakému uživateli, náš kód bude vypadat nějak takhle:
Ha. Funguje to. Něco tu ale smrdí. Ano, jsou to ty dva řádky, které nastavují vazbu mezi Userem a Taskem. Nestačil by jeden? Jak známe kolegu X., určitě někde druhý řádek zapomene (nám by se to samozřejmě nestalo), a pak bude od tohoto bodu dále v paměti strašit inkonzistence dat, až do ukončení databázové ISession. Pak může snadno nastat chyba úplně na jiném místě a než na to přijdeme, ujmou se nás psychoterapeuti. Zkusíme tedy, ach my bláhoví, upravit property Holder:
Ponechme teď stranou fakt, že pokud to samé uděláme v User.Tasks.Add(), nastane nekonečná smyčka kterou ukončí až milosrdné přetečení zásobníku nebo jiná populární výjimka (ve skutečnosti budeme chtít z objektu User publikovat pouze read-only wrapper kolekce Tasks, protože to bude inverzní konec asociace, který není ukládán do databáze). Přejděme mlčky i problém inicializace objektu Task při jeho nahrávání z databáze, kdy kolekce _holder.Task ještě nebude inicializovaná a přidáním tasku do ní si připravíme pozdější muka při nahrávání kolekce z databáze (to se dá řešit, viz např. třída PersistentGenericBag<T> a její metoda DelayedAddAll v NHibernate).
Leč je tu ještě daleko zákeřnější past: this není this které potřebujeme. Jak je to možné? Protože používáme lazy loading našich objektů. Je velmi pravděpodobné, že místo objektu task dostaneme proxy objektu task. Náš milý task proto nebude instance typu Task, ale typu s krásným názvem CProxyTypeTest_BidirectionalTestTaskDomain_NHibernate_ProxyINHibernateProxy1, nebo podobně pojmenovaného. Tento typ je dynamicky vygenerovaný potomek našeho typu Task a slouží jako proxy, která nahraje task z databáze teprve když je to potřeba. Když je proxy třída odvozená z naší třídy, v čem je tedy problém? V tom, že objekt task NENÍ proxy objekt; v programátorštině mezi nimi není referenční identita. Proxy objekt totiž neukládá data do sebe, ale obsahuje referenci na náš objekt a přeposílá mu veškerá volání metod a vlastností (a zpátky předává návratovou hodnotu). Aby to nebylo tak jednoduché, mezi proxy a naším taskem stojí ještě další objekt, tentokrát typu CastleLazyInitializer z NHibernate. Ale to už zabíhám do detailů.
Podívejme se teď znovu na první verzi kódu a přidejme jednu celkem logickou kontrolu výsledku:
Protože se NHibernate chová jako správná Identity Map, vrací v rámci jedné session reference konzistentně, tj. pokud se na několika místech budete dotazovat různým způsobem na jeden Task, dostanete vždy odkaz buď na objekt samotný, nebo na stejnou proxy; mezi vrácenými objekty funguje referenční integrita. To znamená, že user.Tasks.Contains(task) bude fungovat a vrátí správně true.
A nyní se podívejme, co se stane když necháme přidání tasku do kolekce na property setteru Holder:
A zde je jádro pudla: co se ve skutečnosti stane se dá opsat jako task.ProxiedObject.Holder = user;. Uvnitř property setteru Holder proto this ukazuje na náš objekt, ne na proxy. Do kolekce user.Tasks se přidá náš objekt Task, ne proxy v proměnné task. Vypadá to celkem nevinně, protože v tu chvíli už bude náš objekt inicializovaný (přistupujeme na property task.Holder), ale je nutné si uvědomit, že přestává fungovat referenční integrita a tím porušujeme správné fungování Identity Map. Volání metody user.Tasks.Contains(task) na dalším řádku vrátí false, přesto že je náš objekt Task v kolekci a v proměnné task je proxy ukazující na náš objekt. Kolekce user.Tasks prostě obsahuje task.ProxiedObject a ne task.
Zajímavé je podívat se na stack trace v property setteru Task.Holder. Ukazuje, jak se jednoduché volání task.Holder přesměruje přes proxy a interceptor (CastleLazyInitializer):
BidirectionalTest.dll!BidirectionalTest.Task.Holder.set(BidirectionalTest.User value = {BidirectionalTest.User}) Line xxx C#
[Native to Managed Transition]
[Managed to Native Transition]
Castle.DynamicProxy.dll!Castle.DynamicProxy.Invocation.AbstractInvocation.Proceed(object[] args = {Dimensions:[1]}) + 0x76 bytes
NHibernate.dll!NHibernate.Proxy.CastleLazyInitializer.Intercept(Castle.DynamicProxy.IInvocation invocation = {Castle.DynamicProxy.Invocation.SameClassInvocation}, object[] args = {Dimensions:[1]}) + 0xc5 bytes
DynamicAssemblyProxyGen!CProxyTypeTest_BidirectionalTestTaskDomain_NHibernate_ProxyINHibernateProxy1.Holder.set(BidirectionalTest.User value = {BidirectionalTest.User}) + 0xb3 bytes
BidirectionalTest.dll!BidirectionalTest.Bidirectional1NIntegrationTest.Update_ChangesTargetCollection() Line 77 + 0xb bytes C#
A nyní možná trochu překvapivý závěr: Celou tuhle anabázi jsme podstoupili jen proto, že nám nevoněl jeden řádek navíc. Protože se ale synchronizace druhých konců obousměrných asociací (to samé platí i pro many-to-many) ukázala jako plná mnoha problémů, zatím její řešení odsouvám na neurčito. Nemám čas zabývat se problémy, pro které existuje tak jednoduchý workaround. Nejde jen o proxy, ale také o read-only wrappery inverzních konců, rekurzivní smyčky a load time vs. run time, ale také o vlastní typy persistentních kolekcí implementujících IList<T> (místo PersistentGenericBag<T> a spol.), pokud bychom chtěli synchronizovat druhý konec many-to-many asociace při volání Add a Remove (i to je v NHibernate možné, ale je to trochu kostrbaté).
Navíc je tu ještě jedno hledisko, performance. Je třeba si uvědomit, že pokud odebírám prvek z inverzního konce kolekce (nebo přidávám prvek v případě set a idbag), je nutné nejdřív tuto kolekci načíst do paměti, a to většinou zcela zbytečně! Inverzní konce se totiž nijak neukládají. Problém pak ale zůstává, jak chránit další kód ve stejné session proti nekonzistenci dat.
Budu rád pokud zjistím že se pletu a někdo mi napíše jednoduchý způsob jak v NHibernate synchronizaci provést.
Pro začátek si představte, že máte dva objekty, mezi kterými je obousměrná vazba 1-N. Například User a Task, tedy uživatel a jeho úkoly, kde úkol může být přidělen nejvýše jednomu uživateli:
public class User
{
IList_tasks;
public IListTasks
{
get { return _tasks; }
set { _tasks = value; }
}
}
public class Task
{
User _holder;
public User Holder
{
get { return _holder; }
set { _holder = value; }
}
... etc ...
}
Protože nepracujeme ve státním sektoru, Tasků je hodně a je nutné, aby si naše aplikace pamatovala úkoly uživatelů i zítra. Použijeme tedy nějakou databázi; tím myslím existující databázový systém, který si stáhneme z internetu (MS SQL Express, MySQL apod.). Nezačneme tedy implementací vlastního RDMS, ani nebudeme simulovat databázi pomocí mnoha XML souborů uložených kdesi na disku (asi se divíte proč se s tím tak rozepisuju, ale nevěřili by jste, kolik lidí nemá rádo databáze, co si sami nenapsali).
A protože jsme vychytralí, použijeme nějaký O/RM nástroj pro mapování objektů do databáze. A jelikož nejsme bohatí, bude to NHibernate, který je zdarma, a podporuje víc databází než LINQ to SQL (navíc jsme chudí takže stále pracujeme ve Visual Studiu 2005 kde není podpora lambda výrazů).
Když budeme tedy chtít přiřadit Task nějakému uživateli, náš kód bude vypadat nějak takhle:
...
// odněkud vytáhneme volný Task
Task task = TaskRepository.GetNextUnassignedTask();
// najdeme uživatele, který nemá co na práci
User user = UserRepository.GetUserReadingBlogsButNotMine();
// a dáme mu úkol, ať se snaží hošánek (nebo frajerka)
task.Holder = user;
user.Tasks.Add(task); // zachováme obousměrnost vazby, aby se další kód mohl bez problémů odkazovat na user.Tasks
...
Ha. Funguje to. Něco tu ale smrdí. Ano, jsou to ty dva řádky, které nastavují vazbu mezi Userem a Taskem. Nestačil by jeden? Jak známe kolegu X., určitě někde druhý řádek zapomene (nám by se to samozřejmě nestalo), a pak bude od tohoto bodu dále v paměti strašit inkonzistence dat, až do ukončení databázové ISession. Pak může snadno nastat chyba úplně na jiném místě a než na to přijdeme, ujmou se nás psychoterapeuti. Zkusíme tedy, ach my bláhoví, upravit property Holder:
public class Task
{
User _holder;
public User Holder
{
get { return _holder; }
set {
if(_holder!=value)
{
if(_holder!=null)
_holder.Tasks.Remove(this);
_holder = value;
if(_holder!=null)
_holder.Tasks.Add(this);
}
}
}
... etc ...
}
Ponechme teď stranou fakt, že pokud to samé uděláme v User.Tasks.Add(), nastane nekonečná smyčka kterou ukončí až milosrdné přetečení zásobníku nebo jiná populární výjimka (ve skutečnosti budeme chtít z objektu User publikovat pouze read-only wrapper kolekce Tasks, protože to bude inverzní konec asociace, který není ukládán do databáze). Přejděme mlčky i problém inicializace objektu Task při jeho nahrávání z databáze, kdy kolekce _holder.Task ještě nebude inicializovaná a přidáním tasku do ní si připravíme pozdější muka při nahrávání kolekce z databáze (to se dá řešit, viz např. třída PersistentGenericBag<T> a její metoda DelayedAddAll v NHibernate).
Leč je tu ještě daleko zákeřnější past: this není this které potřebujeme. Jak je to možné? Protože používáme lazy loading našich objektů. Je velmi pravděpodobné, že místo objektu task dostaneme proxy objektu task. Náš milý task proto nebude instance typu Task, ale typu s krásným názvem CProxyTypeTest_BidirectionalTestTaskDomain_NHibernate_ProxyINHibernateProxy1, nebo podobně pojmenovaného. Tento typ je dynamicky vygenerovaný potomek našeho typu Task a slouží jako proxy, která nahraje task z databáze teprve když je to potřeba. Když je proxy třída odvozená z naší třídy, v čem je tedy problém? V tom, že objekt task NENÍ proxy objekt; v programátorštině mezi nimi není referenční identita. Proxy objekt totiž neukládá data do sebe, ale obsahuje referenci na náš objekt a přeposílá mu veškerá volání metod a vlastností (a zpátky předává návratovou hodnotu). Aby to nebylo tak jednoduché, mezi proxy a naším taskem stojí ještě další objekt, tentokrát typu CastleLazyInitializer z NHibernate. Ale to už zabíhám do detailů.
Podívejme se teď znovu na první verzi kódu a přidejme jednu celkem logickou kontrolu výsledku:
...
// odněkud vytáhneme volný Task
Task task = TaskRepository.GetNextUnassignedTask(); // vrací proxy objektu Task
// najdeme uživatele, který nemá co na práci
User user = UserRepository.GetUserReadingBlogsButNotMine();
// a dáme mu úkol, ať se snaží hošánek (nebo frajerka)
task.Holder = user;
user.Tasks.Add(task); // do user.Tasks přidáváme proxy objektu Task
// zkontrolujeme zda je task součást uživatelových úkolů
Assert.IsTrue(user.Tasks.Contains(task)); // OK
...
Protože se NHibernate chová jako správná Identity Map, vrací v rámci jedné session reference konzistentně, tj. pokud se na několika místech budete dotazovat různým způsobem na jeden Task, dostanete vždy odkaz buď na objekt samotný, nebo na stejnou proxy; mezi vrácenými objekty funguje referenční integrita. To znamená, že user.Tasks.Contains(task) bude fungovat a vrátí správně true.
A nyní se podívejme, co se stane když necháme přidání tasku do kolekce na property setteru Holder:
...
// odněkud vytáhneme volný Task
Task task = TaskRepository.GetNextUnassignedTask(); // vrací proxy objektu Task
// najdeme uživatele, který nemá co na práci
User user = UserRepository.GetUserReadingBlogsButNotMine();
// a dáme mu úkol, ať se snaží hošánek (nebo frajerka)
task.Holder = user; // task je proxy, ve skutečnosti zprostředkuje volání property setteru našeho objektu
// zkontrolujeme zda je task součást uživatelových úkolů
Assert.IsTrue(user.Tasks.Contains(task)); // FAIL!!!
...
A zde je jádro pudla: co se ve skutečnosti stane se dá opsat jako task.ProxiedObject.Holder = user;. Uvnitř property setteru Holder proto this ukazuje na náš objekt, ne na proxy. Do kolekce user.Tasks se přidá náš objekt Task, ne proxy v proměnné task. Vypadá to celkem nevinně, protože v tu chvíli už bude náš objekt inicializovaný (přistupujeme na property task.Holder), ale je nutné si uvědomit, že přestává fungovat referenční integrita a tím porušujeme správné fungování Identity Map. Volání metody user.Tasks.Contains(task) na dalším řádku vrátí false, přesto že je náš objekt Task v kolekci a v proměnné task je proxy ukazující na náš objekt. Kolekce user.Tasks prostě obsahuje task.ProxiedObject a ne task.
Zajímavé je podívat se na stack trace v property setteru Task.Holder. Ukazuje, jak se jednoduché volání task.Holder přesměruje přes proxy a interceptor (CastleLazyInitializer):
BidirectionalTest.dll!BidirectionalTest.Task.Holder.set(BidirectionalTest.User value = {BidirectionalTest.User}) Line xxx C#
[Native to Managed Transition]
[Managed to Native Transition]
Castle.DynamicProxy.dll!Castle.DynamicProxy.Invocation.AbstractInvocation.Proceed(object[] args = {Dimensions:[1]}) + 0x76 bytes
NHibernate.dll!NHibernate.Proxy.CastleLazyInitializer.Intercept(Castle.DynamicProxy.IInvocation invocation = {Castle.DynamicProxy.Invocation.SameClassInvocation}, object[] args = {Dimensions:[1]}) + 0xc5 bytes
DynamicAssemblyProxyGen!CProxyTypeTest_BidirectionalTestTaskDomain_NHibernate_ProxyINHibernateProxy1.Holder.set(BidirectionalTest.User value = {BidirectionalTest.User}) + 0xb3 bytes
BidirectionalTest.dll!BidirectionalTest.Bidirectional1NIntegrationTest.Update_ChangesTargetCollection() Line 77 + 0xb bytes C#
A nyní možná trochu překvapivý závěr: Celou tuhle anabázi jsme podstoupili jen proto, že nám nevoněl jeden řádek navíc. Protože se ale synchronizace druhých konců obousměrných asociací (to samé platí i pro many-to-many) ukázala jako plná mnoha problémů, zatím její řešení odsouvám na neurčito. Nemám čas zabývat se problémy, pro které existuje tak jednoduchý workaround. Nejde jen o proxy, ale také o read-only wrappery inverzních konců, rekurzivní smyčky a load time vs. run time, ale také o vlastní typy persistentních kolekcí implementujících IList<T> (místo PersistentGenericBag<T> a spol.), pokud bychom chtěli synchronizovat druhý konec many-to-many asociace při volání Add a Remove (i to je v NHibernate možné, ale je to trochu kostrbaté).
Navíc je tu ještě jedno hledisko, performance. Je třeba si uvědomit, že pokud odebírám prvek z inverzního konce kolekce (nebo přidávám prvek v případě set a idbag), je nutné nejdřív tuto kolekci načíst do paměti, a to většinou zcela zbytečně! Inverzní konce se totiž nijak neukládají. Problém pak ale zůstává, jak chránit další kód ve stejné session proti nekonzistenci dat.
Budu rád pokud zjistím že se pletu a někdo mi napíše jednoduchý způsob jak v NHibernate synchronizaci provést.
Přihlásit se k odběru:
Příspěvky (Atom)