Jste zde

Chyby v návrhu: Singleton

V minulém díle jsme se bavili o situacích, kdy má třída příliš mnoho odpovědností. V dnešním díle si ukážeme, jak velké problémy skrývá na první pohled příjemný a jednoduchý návrhový vzor Singleton a proč jej tak mnoho programátorů zneužívá v situacích, kde nemá co dělat.

Úvod

S návrhovým vzorem Singleton se mnoho programátorů již pravděpodobně setkalo. Pro zopakování: jeho záměr je následující (citát je z knihy Design Patterns, často zkráceně nazývána GoF book):

Ensure a class has one instance, and provide a global point of access to it.

Když si to přeložíme, tak použitím onoho návrhového vzoru si zaručíme, že třída bude mít pravě jednu instanci, která bude globálně přístupná.

Mnoho programátorů se k tomuto návrhovému vzoru dostane právě přes onu GoF knihu. Zde bohužel musím souhlasit se Stevem, že většina lidí, kteří si onu knihu přečtou, si z ní odnesou toto: bla bla bla bla Singleton bla bla bla bla bla. Proč tomu tak je? Zřejmě z toho důvodu, že Singleton je pochopitelný i pro programátory, kteří zatím objektově orientovanému programování příliš nepřičichli a stále se drží procedurálního paradigma. Je to jakási záchrana pro lidi, kteří nechápou objektově orientované programování ani poslání oné GoF knihy. Tak si z ní odnesou alespoň Singleton. Jejich reakce je pak podobná této: "Wow, tenhle vzor chápu! To je tak jednoduché. No a jelikož je to návrhový vzor, tak bych ho měl začít pořádně využívat, ať ostatní vidí, že jsem ten správný skilled h4x0r programmer!"

Neberte mě špatně. I já tento návrhový vzor miloval a byl se za něj schopen bít. S postupujícím časem, jak se člověk dostane do objektově orientovaného programování, tak většinou procitne a uvědomí si, že Singleton je zlo a je to jeden z nejvíce zneužívaných návrhových vzorů. Mnoho lidí si totiž myslí, že do svého programu musí nacpat co nejvíce návrhových vzorů. A když jich znají jen pár, či pouze Singleton, tak se jej snaží používat i na místech, kde absolutně nemá co dělat. Zneužití jakéhokoliv návrhového vzoru vede ke špatnému návrhu. Za to ale daný vzor nemůže. Mohou za to programátoři.

No to jsem teda zvědavý. V čem je problém?

Problémů při nevhodném použití tohoto návrhového vzoru vyvstane celá řada.

  • Globální proměnná. Singleton je ekvivalentem globální proměnné v objektově orientovaném prostředí. Možná proto jej tolik lidí miluje, protože globální proměnné znají z Céčka a podobných jazyků a když se dostanou k objektově orientovaným jazykům, jako je Java, tak jim globální proměnné chybí. Nyní asi mnoho z vás namítne, že rovnice Singleton = globální proměnná není pravdivá. Přečtěte si, prosím, záměr tohoto návrhového vzoru z GoF, který jsem uvedl v úvodu. Ano, jeden z jeho záměrů je "provide a global point of access to it".

    Je zajímavé, že většina programátorů z procedurálních jazyků ví, že globální proměnné jsou špatné a v Céčku by se jich štítili jako čert kříže. Ale když se dostanou k objektově orientovanému programování, jak jakoby jim to najednou přestalo vadit a Singletona začnou vesele používat. Zajímavé. Objeví se ale podobné obtíže, jako při použití "klasické" globální proměnné: snížení přehlednosti kódu, znovupoužitelnosti, vysoká provázanost (high coupling) a další. Však to znáte.

  • Schovává závislosti.Tím, že uvnitř metod používáte Singletony, schováváte závislosti. Z rozhraní pak není zřejmé, co daný objekt potřebuje. Dejme tomu, že si uděláte přístup k databázi jako Singleton. Pak nelze ihned říci, které metody přístup k databázi potřebují a využívají, a které nikoliv. Přístup k databázi je totiž interně schován a těžko uhlídatelný, protože nyní se k ní může připojit kdokoliv odkudkoliv.
  • Znesnadňuje testování jednotek. Kód, který používá Singletony, je velmi špatně testovatelný pomocí tzv. testování jednotek, a to především z následujících důvodů.
    • Testovaný kód používá interně něco, co nemůžete mockovat. Ve zkratce onen termín "mockovat" znamená, že v testech použijete falešnou implementaci, která simuluje chování nějakého objektu. Např. pokud váš kód používá databázi, tak v testech můžete použít tzv. mock, díky kterému budete moct kód otestovat, aniž byste měli k dispozici reálnou databázi. Tento přístup má mnoho výhod, především mnohem rychlejší průběh testů (není žádná latence kvůli přístupům k databázi, Internetu apod.) a testování pouze jediné třídy (když testujete třídu, která využívá databázi, tak budete testovat opravdu jen tu třídu, nikoliv to, že vám funguje připojení k databázi, což byste měli otestovat jinde). Singleton se velice špatně mockuje (u většiny implementací to není možné).
    • Jednotlivé testy na sobě začnou záviset. V ideálním případě by mělo být jedno, zda nejdříve spustíte test1 a potom test2, ale při použití Singletonu to nemusí být pravda. Problémem je stav Singletonu, který je sdílen všemi testy. Při typické implementaci Singletonu totiž máte jednu instanci pro celou aplikaci, tudíž pro všechny testy. Jeden test ale může změnit stav Singletonu a druhý s tím nemusí počítat. Musíte tedy nějak zajistit, že po skončení testu se stav Singletonu reinicializuje. Přidání metody reinitialize() je však dost špatný nápad, pokud to děláte jen kvůli tomu, abyste kód mohli otestovat. Co když tu metodu začnou volat uživatelé vašeho Singletonu?

    Pokud navrhujete kód s tím, že má být testovatelný pomocí jednotkových testů, tak místo Singletonu automaticky zvolíte vhodnější řešení. Pokud ale máte netestovaný kód, který je prošpikován Singletony a který chcete testovat, pak vám přeji hodně štěstí a pevných nervů. Budete je potřebovat.

  • Co když je potřeba více instancí? Mnohdy se stane, že zjistíte, že byste potřebovali instancí více. Co pak? Přidáte funkci getInstance2()? Tato situace typicky nastane, pokud začnete zpracovávat něco vícekrát zároveň a potřebujete k tomu Singleton. Pak si ona zpracování navzájem mění stav Singletonu a je s tím více problémů, než užitku. Málokdo si uvědomuje, že je rozdíl, zda vaše aplikace potřebuje pouze jednu instanci či zda může existovat pouze jedna instance. Použitím Singletonu si zbytečně zavíráte vrátka.
  • Nemožnost rozšiřování. Typicky není možné Singletona rozšířit pomocí dědění a virtuálních metod. Všichni uživatelé Singletonu jsou pak vázáni na konkrétní implementaci, což je značně svazující.
  • Porušuje princip jedné odpovědnosti. O principu jedné odpovědnosti (SRP, podle anglického Single Responsibility Principle) jsme si již povídali minule. Pokud použijete klasickou implementaci Singletonu (zprivátnění konstruktoru a přidání statické funkce getInstance()), tak vaše třída najednou bude mít dvě odpovědnosti: (1) tu, kvůli které jste ji vytvářeli a (2) starání se o to, aby měla pouze jednu instanci. Typické porušení SRP.
  • Vysoká provázanost a mnoho závislostí. Mrkněte na tento diagram závislostí. Schválně, jestli v něm najdete dva Singletony :).
  • Implementační problémy. V závislosti na použitém programovacím jazyku se mohou objevit implementační problémy. Pokud je instance Singletonu vytvářena dynamicky, kdo má paměť dealokovat, když není jasné, kdy a kdo všechno ještě Singletona používá? Kdy instanci vytvořit, aby nebyla zbytečně v paměti v případě, že ji nikdo nepoužije? Co takhle serializace a klonování, bude fungovat? Co když přidáme do programu paralelní zpracování? Bude vytváření Singletonu a přístup k němu bezpečný? Co při použití výjimek? Dále, zaručuje použitá implementace, že bude existovat právě jedna instance? Např. v Javě narazíte na problém, pokud použijete více tzv. class loaders, což je běžné u distribuovaných aplikací (Java EE kontejnery, applety atd.). Může se tak snadno stát, že budete mít instancí více, protože každý class loader bude mít svou instanci. Pokud vás zajímají detaily, pak mrkněte např. zde a zde. Suma sumárum, každý jazyk je něčím specifický, takže při použití Singletonů je třeba se dobře seznámit s možnými problémy.
  • Snížení čitelnosti kódu. Použití Singletonu zamoří váš kód zbytečnými voláními getInstance(). Místo
    spooler->addJob(job);

    budete mít

    Spooler::getInstance()->addJob(job);

    Utrpí tím čitelnost vašeho kódu. Schválně si zkuste, kolikrát se ve vašem kódu vyskytuje řetězec "getInstance()".

OK, přesvědčil jsi mě. Co s tím?

Řešení je přímočaré: nepožívat Singleton na místech, kde nemá co dělat. Použití tohoto návrhového vzoru je totiž velmi limitované na speciální případy, které si programátor dokáže ospravedlnit a povede to u nich k lepšímu návrhu, než bez použití Singletonu. Pokud tápete nad tím, zda by na daném místě bylo použití Singletonu výhodné, pak je odpověď ne. Toho ale mnoho programátorů není schopno a pak to dopadá tak, že všude kam se podíváte, tak je Singleton.

Abych uvedl konkrétní příklad nahrazení Singletonu něčím vhodnějším, tak pokud potřebujete pouze jednu instanci, tak si danou třídu instanciujte ve vstupním bodu programu (typicky main) a použijte dependency injection. Pokud tento vzor neznáte, tak se jej naučte, protože to je jeden z nejužitečnějších vzorů. Další tipy viz také odkazy níže.

Kde si o tom můžu přečíst něco dalšího?

Např. na následujících odkazech. Zřejmě vás překvapí, že na tento návrhový vzor tak moc lidí nadává, ale programátoři si za to mohou sami, když jej používají v situacích, kde nemá co dělat.

Nic vám ale nenahradí vlastní prozření, kdy sami na základě zkušeností pochopíte, že Singleton není ta správná cesta. Bude to těžké. Bude to bolet. Ale bude to stát za to.

Přidat komentář