Seriál: Jak se tvoří čistý kód aneb jak se vyvarovat paskvilům – 2. kapitola: Anonymita klienta a zavedení vzoru Singleton s inicializací

V jedné z diskusí na našem serveru se objevil odkaz na zajímavý článek, ve kterém se autor snaží vysvětlit, proč je použití vzoru Singleton nepřípustné a proč jsou Singletony tzv. lháři (v originále „Singletons are Pathological Liars“). V tomto článku si blíže rozebereme tuto problematiku z pohledu tvorby čistého kódu.

Jedním z důsledků principu objektového paradigmatu potažmo tomu odpovídajícímu zapouzdření v OOP je tzv. princip anonymity klienta.

Představme si, že navrhujeme objekt. Ten nabízí své metody (interface) jako služby vůči okolí, tedy komukoliv, kdo ho potřebuje a kdo jej má ve své viditelnosti. Slovo „komukoliv“ (to je vlastně synonymem pro opětovnou použitelnost) zde reprezentuje onoho anonymního klienta, který náš objekt použije a kterého nemáme pod kontrolou. Protože anonymního klienta (uživatele našeho programu, okolní program) nemáme pod kontrolou, je to takříkajíc svobodný člen, nemůžeme (a tedy ani nesmíme) předpokládat jeho chování takové, aby náš objekt fungoval správně. Ať už se chová klient jakkoliv, náš objekt se bude vždy chovat logicky správně.

Náš objekt se musí vždy chovat korektně, i kdyby se na straně klienta objevily chybné kroky (omyl programátora apod.). Nesmíme navrhnout náš objekt tak, aby si vynucoval metodiky pro použití u klienta v tom smyslu, že pro správné a relevantní chování našeho objektu se okolí musí nějak chovat a provádět nějaké kroky, které když neprovede, tak náš objekt přestane fungovat korektně.

Jediný bod, kde si metodicky „podřídíme“ anonymního klienta, je u typových jazyků tvar interfacu, tj. dodržení existence metody v použitém interfacu a následně dodržení dohod pro vstupní a výstupní parametry. Nic víc, než tuto dohodu, nemůžeme po anonymním klientovi žádat, jinak riskujeme, že se náš program spolehne na nespolehlivého anonymního klienta.

Uveďme si triviální příklad na anonymitu klienta: Pro navrhovaný informační systém komunikující přes obrazovky s obsluhou z toho plyne, že se samozřejmě nemůžeme spolehnout, že má-li obsluha napsáno v manuálu, že v políčku nezadá nulu, že ji tam skutečně nezadá. Plyne z toho, že na dodržení pokynů uvedených v manuálu se nesmíme spolehnout a uvedenou funkcionalitu, že zadání nuly „neprojde“, naprogramujeme (a tedy i otestujeme).

Jiným příkladem je zavádění tzv. property (tzv. „getters and setters“ ). Představme si, že nabízíme objekt, který má tři atributy (například rodné číslo, jméno a příjmení) a nabízíme je jako property. Problém je v tom, že po každém zavolání nějakého z těchto property typu setter vždy poté předáme řízení opět anonymnímu klientovi, který není pod naší kontrolou a tak mu předáváme náš „ještě napůl rozpracovaný objekt“ doslova všanc a je na anonymním klientovi, aby vše dopadlo ok. Například klient naplní všechny tři atributy korektně přes settery a následně zavolá nějakou metodu (např. Save). Poté náš klient použije tentýž objekt, ale zavolá pouze set u rodného čísla a hned poté  opět zavolá metodu Save. Půlka objektu je nastavena na novou osobu a půlka na starou osobu, což je nekorektní stav.

Poznámka: Navíc je zřejmé, že nějak ošetřit tuto nekorektní situaci, kdy měníme hodnoty přes settery, není zrovna lehké, tady nám při změně hodnot zjištění, zda atribut není naplněn, moc nepomůže.

Z toho důvodu objektoví puristé OOP kategoricky zavrhují property a naplňují objekt vždy „jedním vrzem“, tj. zavoláním metody se všemi setujícími vstupními parametry a poté vždy až po ukončení dané metody odevzdají klientovi objekt v korektním konzistentním stavu (poznámka: Zde se uplatní vzor Přepravka, viz naše školení in-house OOP a Design Patterns).

Čisté OOP tedy díky anonymitě klienta de facto říká:

Klient by měl vždy před dalším možným použitím objektu (tj. zavoláním metody) dostat objekt v opravdu korektním stavu a neměly by být vydávány nutné pokyny k postupu (který klient může a nemusí dodržet) jen proto, aby náš objekt fungoval korektně.

Plyne z toho, že anonymní klient by neměl být nucen k nějaké metodice použití našeho objektu, která vyžaduje správné použití objektu a když tuto metodiku nedodrží, tak dojde k nekontrolované chybě, ať už v logice chodu našeho objektu anebo k nekontrolovanému pádu programu.

Samozřejmě to neznamená, že vždy vše dopadne dobře ve smyslu, že náš korektně navržený objekt splní vždy všechna přání klienta. Na některá volání může náš objekt v dané situaci reagovat řízenou výjimkou ve smyslu „… ty jsi tuto metodu zavolal zrovna teď, když jsem v tomto stavu, to se mi tedy nelíbí, protože…“ a v popisu námi naprogramované výjimky se klient následně doví,  co se našemu objektu vlastně nelíbí. Programátor – uživatel našeho programu je nucen přečíst si dokumentaci anebo prostudovat kód, proč mu tuto výjimku náš objekt hlásí.

Z toho plyne důležitý závěr:

Řešením problémů s principem  anonymity klienta je důmyslné a též důsledné řešení mechanismu stavů našeho objektu a tedy implicitně také zavedení správného řešení stavů u objektů, které náš objekt používá jako anonymní klient.

A nyní k příkladu v uvedeném článku, kde autor dochází k závěru, že podle jeho slov „Singletons are liars“. Pokud jej nechcete číst celý, podám nyní stručný výklad vysvětlující podstatu tohoto příkladu:

Autor se snaží rozchodit objekt, který se narodí  z třídy CreditCard a zkouší na něm zavolat metodu charge(). Tato metoda mu však bohužel nechodí.

Zjišťuje, že k tomu, aby chodila, musí být inicializován objekt CreditCardProcessor v pozici Singletonu, ale i tak mu to nechodí, protože tento objekt používá další objekt zvaný OfflineQue jako další Singleton, takže musí být inicializován i tento objekt, který však potřebuje objekt Database jako další Singleton, který také musí být inicializován.

Takže po strastiplné pouti neošetřených výjimek se nakonec metodou pokus omyl dostane až k chodícímu kódu ve tvaru:

testCreditCardCharge() {
Database.init();
OfflineQueue.init();
CreditCardProcessor.init();
CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
c.charge(100);
}

Navíc je zřejmé, že sekvence inicializací objektů musí proběhnout zrovna takto a nijak jinak, například nemůže proběhnout v jiném pořadí, program nesmí vynechat žádnou z nich apod.

Nakonec autor dospěje k závěru, že lepší cestou je předávat si potřebné objekty jako parametry přes interface s inicializací potřebných objektů, např. takto:

testCreditCardCharge() {
Database db = Database();
OfflineQueue q = OfflineQueue(db);
CreditCardProcessor ccp = new CreditCardProcessor(q);
CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
c.charge(100);
}

Nakonec z toho autor vyvozuje, že „Singletons are liars“, protože schovávají závislosti.

Je to sice pěkný příklad ukazující na problémy s inicializací objektů, až na to, že všechny uvedené nepříjemnosti zde podle mého soudu nevznikají proto, že se jedná o viditelnost typu Singleton, ale jednoduše proto, že nebyl dodržen princip anonymity klienta podobně jako v našem příkladu se settery u objektů typu Osoba.

Celý problém je v tom, že uvedená nutná sekvence po sobě jdoucích inicializací je přenechána na anonymním klientovi (tj. zde na programu autora článku) a proto nastaly tyto nepříjemné strasti při rozchození programu. Klient totiž dostal objekt v nekorektním stavu a z toho důvodu mu metoda charge() spadla na neřízené výjimce (resp. následně na dalších neřízených výjimkách při dalších pokusech rozchození programu u „nižších“ objektů).

Nakonec zjišťujeme, že problém je v tom, že klient by k použití našeho objektu ze třídy CreditCard musel dostat navíc i metodický pokyn ve smyslu: Chceš použít objekt ze třídy CreditCard? Tak nejprve musíš zavolat sekvenci inicializací objektů takto a nijak jinak:

//...
Database.init();
OfflineQueue.init();
CreditCardProcessor.init();
//...
}

jinak ti to spadne.

A to je názorný příklad na porušení anonymity klienta.

Jaké je tedy správné řešení zohledňující princip anonymity klienta?

Existuje několik možných řešení, všechny však mají jeden společný požadavek vyplývající z principu anonymity klienta : Je třeba implementovat inicializaci u všech závislých objektů za hranicí daných objektů, tj. nenechat inicializaci na metodice pro anonymního klienta.

Důležitá poznámka: Při hledání správného řešení je třeba si uvědomit, že princip anonymity klienta platí vždy a všude. Znamená to, že i v našem příkladu každý používající objekt je anonymem z pohledu každého používaného objektu.

První řešení je zavést inicializaci přímo při volání u prvního objektu. Například zavoláním charge u objektu z třídy CreditCard si on sám posoudí, zda je inicializován, pokud ne, tak se inicializuje, přitom zavolá inicializaci u pro něj potřebných objektů včetně těch, které jsou v pozici Singleton (v příkladu má za sebou ve svém použití jen jeden Singleton ze třídy CreditCardProcessor). Pokud jsou tyto objekty nastaveny na stav „neinit“, tak se inicializují, přitom zavolají v sekvenci další objekty včetně Singletonů, které potřebují atd. Takto daná inicializace „putuje“ z volající vrstvy kódu do vrstvy volaného kódu (tranzitivně dál a dál). Nakonec pokud se všechna inicializace všech vnořených volání zdaří, tak algoritmus inicializace vyblublá zpět do objektu card a teprve poté se provede algoritmus dané metody charge. Pokud je objekt card již korektně inicializován (tj. je v tomto stavu), tak se vynechá inicializace a rovnou se přikročí k algoritmu charge.

Je třeba ještě poznamenat, že z důvodu anonymity klienta jsou všechny stavy všech objektů nejen zapouzdřeny, ale v žádném případě nejsou nastavitelné přes settery, ale mění se pouze jako výsledek volání zmíněných inicializací.

Předešlou variantu můžeme u objektů v pozici Singleton doplnit o další jednoduchou možnost řešení: U všech objektů v pozici Singleton budeme implementovat inicializaci daného objektu Singleton přímo ve statické metodě poskytující objekt Singletonu. Podle klasického vzoru se v této metodě pouze zjišťuje, zda je objekt vytvořen a pokud ne, tak se vytvoří a poté se předá jako výstupní parametr této statické metody. V našem případě by byl kód ve větvi „objekt není vytvořen“ doplněn navíc také o inicializaci objektu takto (viz řádek 4):

public static SingletonWithInit getInstance() {
if(instance == null) {
instance = new SingletonWithInit();
instance.Init(); // new pattern code
}
return instance;
}

 Je vcelku zřejmé, že metoda Init() někde v sobě volá statickou metodu (resp. metody)  getInstance() u těch objektů včetně Singletonů, které tento náš objekt Singleton potřebuje ke své činnosti.

Toto rozšíření vzoru o inicializaci se tedy uplatní u všech tří objektů typu Singleton v našem příkladu (tj. pro objekty ze tříd CardProcessor, OfflineQueue a Database). Sekvence inicializace pak v našem příkladu vypadá nějak takto:

  1. Objekt card ze třídy CreditCard potřebuje Singleton CardProcessor, ve své inicializaci tedy zavolá statickou metodu CardProcessor.getInstance(). Tím se podle předešlého kódu rozšířeného vzoru SingletonWithInit spustí metoda Init() objektu CardProcessor (pokud objekt již není zrozen a inicializován).
  2. Objekt CardProcessor potřebuje objekt typu Singleton ze třídy OfflineQueue, proto ve své metodě Init() volá OfflineQueue.getInstance(), tím spustí metodu Init() u objektu ze třídy OfflineQueue (pokud tento objekt již není zrozen a inicializován).
  3. Objekt OfflineQueue potřebuje objekt typu Singleton ze třídy Database, proto metoda Init() tohoto objektu zavolá Database.getInstance() a tím spustí Init() tohoto objektu (pokud objekt již není zrozen a inicializován).
  4. Objekt Database již nepotřebuje žádný Singleton, nebude tedy volat ve své inicializaci žádné getInstance() nějakého Singletonu, provede pouze své potřebné kroky k inicializaci.

Druhým řešením, jak zohlednit princip anonymity klienta v našem příkladu, je zavést uživatelské výjimky. Nedocházelo by takto k překlápění stavů potřebných objektů, ale pokud někdo zavolá objekt, který není správně inicializován, tak objekt vyhodí uživatelskou výjimku přesně definovanou ve smyslu „jsem ten a ten a nejsem v inicializovaném stavu“. Výsledkem tohoto řešení je trochu  nepříjemnější cesta programátora – uživatele našeho programu, který zkouší rozchodit program a ten mu „padá“, ale již na uživatelských výjimkách s popisem „proč“. Nakonec tak trochu se skřípěním zubů a nutného studia dokumentace resp. kódu nakonec programátor kód rozchodí. Je zřejmé, že toto řešení sice splňuje anonymitu klienta, ale pro programátora, tj. uživatele našeho objektu, je o hodně nepříjemnější. Proto pokud existuje schůdná a lepší varianta (která v našem případě existuje), tak raději zvolíme tuto, kdy uživatel našeho objektu použije náš objekt jednodušeji.

Možná je i kombinace obou přístupů: Například objekt card (který není v pozici Singleton) vyžaduje volání metody init před voláním metody charge, jinak vyhodí řízenou výjimku. Toto volání init u tohoto dynamického objektu card vyvolá kaskádu initů u všech potřebných Singletonů v řadě za sebou (například jak je uvedeno výše přes získání objektu Singletonu voláním statické metody getInstance).  Pokud tato kaskáda proběhne, objekt card se dostane do stavu jsme inicializován. Volání charge u objektu card poté pouze ověří, zda daný objekt je ve stavu inicializován, pokud ne, vyhodí řízenou výjimku.

Třetí možnost je uvedena  v příkladu v článku, tj. zavede se předání závislostí mezi objekty pomocí vstupního parametru metody, což je také možné. Publikováním závislosti do interfacu vlastně předáváme nutnost pro chování anonymního klienta (tj. „vynucený metodický pokyn“) do již zmíněné dohody s interfacem. Avšak na daném řešení přes předávání závislých objektů pomocí parametrů přes interface se mi nelíbí jedna podstatná věc (je to věc k posouzení v diskusi): Nadbytečné (resp. nahraditelné a tedy zbytečné) publikování závislostí do parametrů metod vede k větší otevřenosti programu vůči vnitřní implementaci objektu a k nepříjemnostem s budoucí flexibilitou, protože klient se stává více závislým na vnitřní implementaci našeho objektu. Představme se, že dalším vývojem se program takříkajíc překope a po této změně již daný objekt X (ať už Singleton anebo náš klasický dynamický objekt) nepotřebuje pouze objekt Y, ale také objekt Z. V dané metodě by se tedy předávaly nikoliv jeden, ale již dva parametry jako objekty a samozřejmě tato změna se týká všech klientů, kteří by objekt X používali.

Tato nevýhoda je předmětem diskuse (viz možnost pod článkem).

Mně osobně se líbí varianta s inicializací objektů v každé vrstvě, tj. takové řešení, kdy každá vrstva kódu si řídí svoje stavy a při volání „zvně“ od vyšší vrstvy si sama posoudí svůj momentální stav a podle toho se sama rozhodne, jak reagovat, takže vyšší vrstva se vlastně o nic nestará (například viz rozšíření vzoru Singleton o inicializaci vznikajícího objektu).

Závěr

Podle mého soudu Singletony nejsou „liars“ v pravém slova smyslu, jsou pouze „dangerous“, protože vyžadují vyšší nároky na dodržení principu anonymity klienta, neboť anonymním klientem může být opravdu „kdokoliv“  v celém programu, kdo má daný Singleton ve své viditelnosti včetně jiných Singletonů.

Doporučený odkaz

Věnujte pozornost rozšíření školení OOP a Design Patterns o kapitoly čistý návrh OO, dočasně se zaváděcí cenou bez žádného navýšení, blíže viz zde.

Konec článku


Uveřejněno

v

,

od

Značky:

Komentáře

2 komentáře: „Seriál: Jak se tvoří čistý kód aneb jak se vyvarovat paskvilům – 2. kapitola: Anonymita klienta a zavedení vzoru Singleton s inicializací“

  1. Karel Král avatar
    Karel Král

    Pravé singletony jsme opustili asi před 8 lety a jsme za to vděční. Je to nekonečný zdroj problémů. Nahradili jsme je dependency containerem (Unity) s registrací objektu jako singletonu. Získali jsme samé výhody a od té doby singleton nechceme ani vidět. Ať s inicializací při vytvoření nebo bez. Dependency container vám např. automaticky vyřeší celý řetězec závislostí a inicializací… Stačí správný návrh a správné registrace.

    1. RNDr. Ilja Kraval avatar

      samozřejmě, článek není nějakou obhajobou používat Singletony za každou cenu, však také v prvním článku se zmiňuji o nevýhodách Singletonu, zejména v jeho statické metodě, kterou musíme použít a která je příliš „tvrdá“ a nelze ji překrýt.
      Tento článek je o tom, že uvedený příklad v článku o Singletonech jako lhářích je o něčem jiném, než o nevýhodě Singletonu, protože sám objektový návrh je objektově nečistý. O tom je článek.
      Jak je uvedeno v článku, jedním z řešení dodržení principu anonymity klienta u závislostí je předávání objektů jako parametry (i s možnými nevýhodami).
      To, že nemáte problémy, je určitě dobře, jen bych podotkl, že 99% problémů se Singletonem (pokud pominu onu statickou „tvrdou metodu“, kterou nelze překrýt) je právě v nedodržení anonymity klienta, to se velice podceňuje bohužel nejenom u Singletonů a vede to k nestabilitám programu.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *