Je singleton zlo?
Singleton je jedním z nejpopulárnějších návrhových vzorů. Jeho úkolem je zajistit existenci pouze jediné instance určité třídy a zároveň poskytnout globální přístup k ní. Pro úplnost malý příklad:
class Database
{
private static $instance;
private function __construct()
{}
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self;
}
return self::$instance;
}
...
}
// singleton je globálně dostupný
$result = Database::getInstance()->query('...');
Typickými rysy jsou:
- privátní konstruktor, znemožňující vytvoření instance mimo třídu
- statická vlastnost $instance, kde je unikátní instance uložená
- statická metoda getInstance(), která zpřístupní instanci a při prvním volání ji vytvoří (lazy loading)
Jednoduchý a snadno pochopitelný kód, který řeší dva problémy objektového programování. Přesto v dibi nebo Nette Framework žádné singletony nenajdete. Proč?
Unikátnost jen zdánlivá
Podívejme se na kód pozorněji – skutečně ručí za existenci pouze jedné instance? Obávám se, že nikoliv:
$dolly = clone Database::getInstance();
// nebo
$dolly = unserialize(serialize(Database::getInstance()));
// nebo
class Dolly extends Database {}
$dolly = Dolly::getInstance();
Proti tomu existuje obrana:
final public static function getInstance()
{
// finální getInstance
}
final public function __clone()
{
throw new Exception('Clone is not allowed');
}
final public function __wakeup()
{
throw new Exception('Unserialization is not allowed');
}
Jednoduchost implementace singletonu je ta tam. Ba co hůř – s každým dalším singletonem se nám bude opakovat kusanec stejného kódu. Také třída najednou plní dva zcela odlišné úkoly: kromě svého původního úkolu se stará o to být dost single. Obojí je varovný signál, že něco není v pořádku a kód by si zasloužil refaktoring. Vydržte, za chvíli se k tomu vrátím.
Globální = ošklivé?
Singletony poskytují globální přístupový bod k objektům. Není nutné si odkaz na ně neustále předávat. Zlí jazykové však tvrdí, že taková technika se nijak neliší od používání globálních proměnných a ty jsou přece čiré zlo.
(Pokud metoda pracuje s objektem, který jí byl nějak explicitně předán, parametrem nebo jde o proměnnou objektu, říkám tomu „drátové spojení“. Pokud pracuje s objektem, který si získá přes globální bod (např. skrz singleton), říkám tomu „bezdrátové spojení“. Docela fajn analogie, ne?)
Zlí jazykové se v jedné věci mýlí – na „globálním“ není a
priori nic špatného. Stačí si uvědomit, že název každé třídy a metody
není nic jiného, než globální identifikátor. Mezi bezproblémovou
konstrukcí $obj = new MyClass
a kritizovanou
$obj = MyClass::getInstance()
není ve skutečnosti zásadní
rozdíl. Tím méně u dynamických jazyků jako je PHP, kde lze psát $obj = $class::getInstance()
.
Co ale může způsobit bolení hlavy, to jsou
- skryté závislosti na globálních proměnných
- nečekané používání „bezdrátových spojení“, které z API tříd nevyčtete (viz Singletons are Pathological Liars)
První bod se dá eliminovat, pokud se nebudou singletony chovat jako
globální proměnné, ale spíš jako globální funkce či služby. Jak tomu
rozumět? Vezměte si třeba google.com – hezký příklad singletonu
jakožto globální služby. Existuje jedna instance (fyzická serverová farma
kdesi v USA) globálně dostupná přes identifikátor
www.google.com
. (Dokonce ani clone www.google.com
nefunguje, jak zjistil Microsoft, maj to holt kluci vychytaný.) Důležité je,
že tato služba nemá skryté závislosti typické pro globální
proměnné – vrací odpovědi bez neočekávatelné souvislosti s tím, co
před chvíli hledal někdo úplně jiný. Naopak nenápadná funkce strtok trpí vážnou závislostí na
globální proměnné a její používání může vést k velmi těžko
odhalitelným chybám. Jinými slovy – problém není v „globálnosti“,
ale v návrhu.
Také druhý bod je čistě záležitostí návrhu kódu. Není chybou použít „bezdrátové spojení“ a přistoupit ke globální službě, chybou je to dělat proti očekávání čili skrytě. Programátor by měl přesně vědět, jaký objekt využívá které třídy. Poměrně čistým řešením je mít v objektu proměnnou odkazující na objekt služby, která se inicializuje na globální službu, pokud programátor nerozhodne jinak (technika convention over configuration).
Unikátnost může být na škodu
K singletonům se váže problém, na který narazíme nejpozději při
testování kódu. A tím je potřeba podstrčit jiný, testovací objekt.
Vraťme se ke Google jakožto ukázkovému singletonu. Chceme otestovat
aplikaci, která jej využívá, jenže po pár stech testech začne Google
protestovat We're
sorry… a jsme kde? A jsme někde. Řešením je pod identifikátor
www.google.com
podstrčit fiktivní (mock) službu. Potřebujeme
upravit soubor hosts
– jenže (zpět z analogie do světa OOP)
jak toho dosáhnout u singletonů?
Jednou z možností je implementovat statickou metodu
setInstance($mockObj)
. Ale ouha! Copak asi chcete slečně metodě
předat, když žádná jiná instance, než ona jedna jediná,
neexistuje?
Jakýkoliv pokus odpovědět na tuto otázku povede nevyhnutelně k rozpadu všeho, co singleton dělá singletonem.
Pokud odstraníme restrikce na existenci jen jedné instance, přestane být
singleton single a my řešíme pouze potřebu globálního úložiště. Pak je
nabíledni otázka, proč v kódu stále opakovat stejnou metodu
getInstance()
a nepřesunout ji do extra třídy, do nějakého
globálního registru?
Anebo ponecháme restrikce, pouze identifikátor v podobě třídy
nahradíme za interface (Database
→ IDatabase
),
čímž se vynoří problém nemožnosti implementovat
IDatabase::getInstance()
a řešením je opět globální
registr.
O pár odstavců výše jsem sliboval vrátit se k otázce opakujícího se kódu ve všech singletonech a možnému refaktoringu. Jak vidíte, problém se mezitím vyřešil sám. Singleton zemřel.
Komentáře
Martin Malý #1
Počkej, bojovníci proti globalizaci ti dají co proto. Globální úložiště! Ještě dodej, že to je úložiště vyhořelého jaderného paliva a máš na krku i Kuchtu… :)
A než dorazí, tak se zeptám: „mít v objektu proměnnou odkazující na objekt služby, která se inicializuje na globální službu, pokud programátor nerozhodne jinak“ je idea pěkná. Otázka je: KDY „se inicializuje“?
Nejsem příručkový programátor, tak si to řeším „klasicky“, nikoli class-scope proměnnou, a informaci o tom, že si volám cizí třídu, si ukládám nějak takhle:
/**
* class Foo
* @uses Bar, Dar, Tatar
*/
což je to, o co jde – dát najevo, že v třídě je použita odvolávka na nějakou globální singlovku. Jen podotek, no flame… :)
David Grudl #2
#1 Martin Malý, to je hodně dobrý nápad! Ještě to chce zvolit takovou
@proměnnou
, aby to phpDocumentor nebo jiný konvertor zachoval i ve vygenerované dokumentaci. Nebo ohnout phpDocumentor?Michal Aichinger #3
Nezda se mi, že zavrhuješ kvůli testováni singleton, i když u tebe je možné vše ;) Co ten mock objekt podstrčit o úroveň výše? Možností je více. Pokud tvoříš jakýkoli objektovou aplikaci, tak aby byla úplně objektová je třeba aby vše bylo zastřešeno jedním objektem, např. Application. A do něj si ukládáš instance těhle jedináčků, takže stačí při inicializaci aplikace podstrčit mock jedináčky. (Pro testování třídy aplikace pak samozřejmě máš mock aplikační objekt ;))
Nebo jde postupovat druhou cestou, kdy tyhle jedináčky získáváš z nějaké továrny (Factory) a pak samozřejmě v závislosti na konfiguraci podstrkuješ mock, nebo ne.
Ondrej Ivanic #4
V PHP sa v podstate Singleton ani vytvoriť nedá, jeho jedinečnosť je zaručená maximálne v rámci skriptu (max. session, ak je jej sprava ponechaná na PHP). Ak by PHP malo aplikačný kontajner (čo nemá) tak by to už možno stalo za uváženie.
Neviem, že prečo každý ukazuje v PHP Singleton na databázovej triede? Je to zavádzajúce! Ak získam DB objekt (::getInstance()) a spustím query tak ďalší kto ziska tuto inštanciu nemá možnosť spustiť query pokiaľ ta prvá neskončí, alebo implementoval connection pool. V PHP sa však nič takéto „bizardné“ nenastane, lebo v PHP sa skutočný Singleton vytvoriť nedá (chýba napr. ten aplikačný kontajner).
Ako bolo na konci spomenute najlepsie riesenie je Registry do ktoreho sa daju potrebne objekty postrkat. V dokumentacii sa len uvedie ze trieda XYZ vyuziva triedu ktora musi implementovat rozhranie IABC a bud je predana cez jej konstruktor, alebo je hladana v Registry pod klucom ‚iabc‘. No easy, nie? Zaroven odpadnu aj starosti s mock-ovanim objektov a depencency injection co je hit toho leta.
Ondrej Ivanic #5
#2 David Grudl, To je už vyriešené pomocou @see
https://manual.phpdoc.org/…see.pkg.html
David Grudl #6
#3 Michal Aichinger, co myslíš tím „podstrčit o úroveň výš“?
Singleton zavrhuji z důvodu, že neexistuje situace, kdy by objekt sám o sobě mohl dospět ke správnému rozhodnutí, že může existovat jen jednou. Toto rozhodnutí přesouvám na aplikace.
#4 Ondrej Ivanic, ad „…trieda XYZ vyuziva triedu ktora musi implementovat rozhranie IABC a bud je predana cez jej konstruktor, alebo je hladana v Registry pod klucom ‚iabc‘.“ Přesně tak.
Michal Aichinger #7
#6 David Grudl, Kód píše programátor nebo se kód píše sám? Pokud ho píše programátor, pak asi ví proč na některém místě chce použít jedináčka a kde ne. Takže tvé tvrzení kdy by objekt sám o sobě mohl dospět ke správnému rozhodnutí mi přijde trošku zavádějící.
#4 Ondrej Ivanic, PHP globální sdílení dat mezi uživatelskými sezeními nemá – aplikačný kontajner. Ale ruku na srdce, kolik si např. do ASP.NET aplikace napíšete jedináčků, které budou sdíleny mezi různými session, určitě skoro žádného, většinou to bude jen session/page view jedináček.
David Grudl #8
#7 Michal Aichinger, Tomu říkám definitivní argument. Ten lze použít kdykoliv a na cokoliv. Díky!
petr #9
ad #6.1 zcela seriózně: mohl by to kdyžtak někdo více rozvést? téhle části jsem moc neporozuměl. jinak super článek
Martin Malý #10
#9 petr, Jak tomu rozumím já: Nově vytvořený objekt nemá šanci zjistit, že neměl být vytvořený, že měla být jen vrácena instance na nějaký již existující. Například při onom zmíněném klonování nebo deserializaci.
Jistý „workaround“ by tu možná šel, ale je to takové… Šlo by udělat a udržovat statickou proměnnou třídy, která by obsahovala informaci o počtu instancí (prakticky tedy jen 0 nebo 1). Ovšem těžko si představit, co by objekt měl dělat ve chvíli, kdy by zjistil, že je „druhý“…
Ty objekty mívají občas téměř lidské starosti, že? Jak pravil Mao Ce-Tung, klasik objektového programování: Třídy bojují, některé vítězí, jiné jsou odstraněny!
The Zero #11
Ačkoliv pointa je někde jinde, než ve zbytečné duplikaci kódu, od PHP 5.3 půjde udělat třída Singleton, ne?
Otázka ovšem je, jestli vůbec půjde udělat ty třídy, které potřebujeme mít singletonem, tak, aby dědily Singleton, protože často už nějakého předchůdce mít budou.
David Grudl #12
#9 petr, #10 Martin Malý myslel jsem to trošku jinak.
Z hlediska návrhu aplikace je odvážné přisoudit objektu/třídě vlastnost, že může existovat jen jednou. Zdánlivě to smysluplné je – třeba právě to zapouzdření připojení k databázi. Ale v realitě se pak ukáže, že jde spíš o nedomyšlení věci. Co když potřebujeme přístup ke dvoum databázím, co když potřebujeme k jedné databázi dvě spojení kvůli transakcím? A v podstatě vždycky může vzniknout potřeba mock objektu kvůli testování.
Třída může rozhodovat o tom, které její metody a vlastnosti budou privátní a které veřejné, to je její píseček a tady platí její slovo. Ale do toho, co se děje v globálním prostoru, by už mluvit neměla. Tam už má rozhodovat nějaká vyšší logika.
Tomik #13
Můžu se zeptat, když už jsme u těch singletonů, zda jej někdo z vás někdy opravdu použil, myslím, kromě takových těch papírových příkladů, opravdu v praxi?
Vím, že toto existuje, ale nikdy jsem se neocitl v situaci, kdy bych jej potřeboval, nebo by mi usnadnil život. Ptám se tedy, zda jsem o něco přišel? Nebo mi něco uniká?
Chci tím říct, neztrácí se pak vpodstatě kouzlo OOP? Proč vytvářet třídu, když může mít jen jednu instanci, nemůže se dědit, klonovat?
Myslím, že asi vždy může nastat situace, kdy je dobré mít min. dvě instance jedné třídy.
David #14
#13 Tomik, Ano, máte naprostou pravdu. V každém svém kódu vytvářím minimálně dvě (někdy tři a více, dle nálady) instancí každé třídy.
Proto je pro mne Singleton pattern naprosto mrtvá věc.
😁
Martin Hassman #15
„Z hlediska návrhu aplikace je odvážné přisoudit objektu/třídě vlastnost, že může existovat jen jednou.“
A co když to Davide je zrovna v tom konkrétním případě správný objektový model reality? Pak to může nejen zjednodušit kód, ale učinit rozhraní srozumitelnějším (pokud dostanu nový framework a některé třídy v jeho rozhraní jsou singletony, pak mi už ta struktura trochu napovídá, jak to mám či nemám používat = intuitivní ovládání).
Opustím vody PHP. Co třeba objektový model Mozilly (Firefoxu)? Obsahuje spoustu singletonů, např. objekt pro správu záložek je singleton, protože i když k němu lze přistupovat současně z mnoha míst programu (např. mám otevřené okno správce záložek s rozeditovanými údaji, vedle toho v hlavním okně Firefoxu zadávám klávesovou zkratku pro přidání nové záložky, pak nějaké rozšíření na pozadí zrovna synchronizuje mé záložky s del.icio.us a možná druhé rozšíření, o kterém nevím, právě posílá mé záložky na kontrolu někam, třeba do FBI), jsou záložky fyzicky jen jednou databází = jedním souborem, nelze k němu přistupovat současně najednou a na nějaké úrovni se tenhle konflikt musí vyřešit. Řešení udělat z přístupového objektu singleton mi třeba tady připadá na místě.
Schválně jsem poodešel od PHP, protože v něm jsem nikdy neprogramoval na takové úrovni, abych o singletonech byť jen uvažoval. Obecně v objektovém návrhu singleton má smysl. Ale samozřejmě je kravina snažit se ho cpát někam neopodstatněně jen protože si myslím, že tenhle objekt asi nebudu vytvářet vícekrát, spíš má místo tam, kde daný objekt prostě logicky nemůže a NESMÍ mít víc instancí.
klok #16
#12 David Grudl, singleton je ze sve podstaty urcen jako pristup k unikatnim zdrojum. Coz v PHP zas az tak moc smysl nedava. Presto je v urcitych chvilich i v PHP singleton smysluplnym navrhovym vzorem.
Co se tyce moznosti clone() a podobne, vetsina programatoru tento pattern zna, takze se nebude pokuset vytvaret novou instanci pres clone, ostatni by si meli precist alespon priklad pouziti tridy 😉
Michal Aichinger #17
#12 David Grudl, #15 Martin Hassman
Jak píše klok. Jde o přístup k unikátním zdrojům. Samozřejmě mít singleton pro přístup k DB je často nevhodné. Ale třeba aplikační kontejner (#4 Ondrej Ivanic) bychom asi rádi na serveru měli jako singleton ve smyslu jednoho rámce pro všechny aplikace.
Je do spíš o myšlení programátorů. #13 Tomik Představte si, že máte monitorovací aplikaci teploty. Teplotu měří jeden teploměr. V aplikaci s touto teplotou operujete na desítkách míst, tak jako objektovou reprezentaci je vhodné si udělat singleton pro teploměr.
Obecně je nutné kód předávat s dokumentací a když programátor zamýšlí pro svoji třídu singleton a zakáže konstruktor, tak to asi má smysl. Pokud nějaký jiný to obejde klonováním, pak je to špatně a důsledky si musí řešit sám.
Navíc nevím, proč se rozmáhá tvorba jedináčka formou
self::$instance = new self;
do statické privátní proměnné třídy. To samozřejmě nezaručuje jedinečnost, viz problém s clone, atd. Pokud ale budeme chtít jedináčka v rámci stránky, ukládáme do globální proměnné, pokud v rámci sezení, ukládáme do session.
Tomik #18
#17 Michal Aichinger, Dobrá, to je rozumný případ. Ale vážně budete psát driver pro teploměr v PHP? Ale faktem je, že kdyby tomu tak bylo, dává singleton smysl.
Jakub Vrána #19
#1 Martin Malý, To globální úložiště přeci může mít podobu
Registry::Database()
– tam už bude klasický jedináčkovský test. Nebo pokud bych trval na použití vlastnosti, tak přes přetěžování. Idea lazy load tím zůstane zachovaná.Martin Malý #20
#13 Tomik, Měl jsem krásnou odpověď, ale poněkud nakynula, tak jsem ji napsal k sobě
David Grudl #21
Můžete vymýšlet další tisíce příkladů na singletony a já vám vždy odpovím, že potřebuju testovat kód a jste nahraní. Co je na tom tak těžkého pochopit?
(ps. omlouvám se za občasné požírání komentářů, testuju antispam)
Aleš Roubíček #22
#21 David Grudl, To není nic proti ničemu. I se singletony můžeš psát plně testovatelný kód… Jen je potřeba správně řídit závislosti jednotlivých komponent.
LLook #23
Přemýšlím nad těmi argumenty o unikátních zdrojích… A nenapadá mě příklad z praxe, kdy unikátnost přístupu není vhodnější zabezpečit na nižší, systémové, úrovni.
Tak třeba ta databáze pracuje se soubory, které lze zamykat.
Teploměr je pro změnu vzdálená služba. Stejně jako není důvod singletonovat HTTP klienta, tak nevidím důvod singletonovat klienta teploměru. O unikátnost přístupu se postará server (webový nebo teplový, to je jedno), který opět k synchronizaci může využít systémové prostředky.
Martin Malý #24
#19 Jakub Vrána, Mám dojem, že jsme si trošku nerozuměli. Já polemizoval s Davidovým řešením onoho problému „není na první pohled vidět, že si třída šahá někam pro nějakého singletona“. Davidovo řešení hovoří o tom, že si definuje proměnnou, hezky nahoře, a napíše k ní jaký typ má, aby bylo vidět, že je tento typ v třídě použit. Já se ptal na některé implementační detaily takového přístupu, a navrhoval jsem za stejným účelem použít možností phpDocumentoru, protože ten plní účel („říct, že tahle třída něco volá“) pravděpodobně líp.
To nijak nesouvisí s globálním úložištěm, bych řekl, a to, že jsem ho zmínil, je jen interní žertík.
Martin Malý #25
#23 LLook, Narážíme na rozdílnost pojmu „aplikace“ u klasických a u WWW technologií. „Singleton“ u serverového řešení např. v Javě je něco jiného než „singleton“ v PHP, protože „aplikace“ je v Javě něco jiného než „aplikace“ v PHP. Životnost většiny objektů v PHP (hovořím o klasické WWW serverové aplikaci) je „request-scope“, tedy vznikají s dotazem na HTTP server a s ukončením běhu skriptu zanikají. Mohou ale (při použití nějakých obezliček) být „session-scope“, tedy trvat od prvního přístupu uživatele až po jeho odchod, a teoreticky mohou být i „application-scope“, tedy sdílené všemi sezeními, všemi serverovými vlákny… Problém je, když někdo začne v PHP operovat pojmy jako „globálně unikátní“, které jsou např. u serverové aplikace v Javě víceméně bezrozporné… Co to ale znamená v PHP? Je to „globálnost po dobu běhu skriptu“? Nebo „globální pro všechna vlákna serveru“? Nebo snad „globální a perzistentní po celou dobu uživatelského sezení“? Nebo jen „globální, ale pro každé sezení zvlášť“?
Bohužel, na tenhle rozdíl mezi skriptovanými serverovými vícevláknovými aplikacemi a mezi aplikacemi „monolitními“ (byť vícevláknovými, ovšem vlákna stále v rámci aplikace) se v podobných obecně programátorských diskusích často zapomíná, a pak vznikají zmatky… zato nám ten flame hezky hoří! :)
David Grudl #26
Koukám, že tu dochází k nedorozuměním, takže ještě jednou a polopatě:
Padla tu řada důvodů, proč je vhodné/šikovné/užitečné mít jednu instanci globálně dostupnou, ať už jakýmkoliv mechanismem. Ale zatím nepadl žádný důvod obhajující to „právě jednu“.
Michal Aichinger #27
#21 David Grudl, Jak testovat jsem ti již napsal ve svém příspěvku #3 Michal Aichinger.
Martin Malý #28
#26 David Grudl, AHA! :)
Teď mi secvaklo. Proč právě jednu?
Vem si jako příklad ten mnou zmiňovaný objekt Response. Můžeš jich udělat neurekom, ale na konci půjde na výstup právě jeden. Který? No, pravděpodobně ten globálně dostupný, do něj přeci aplikace průběžně zapisovala. Je to nejjednodušší a „logické“, že?
Může jich být víc? Může, ale nemají smysl. Pokud nemají smysl, neměly by být vůbec vytvářeny… Kupříkladu onen „výstup“ (Response) by měl být právě jeden.
Oponuji si: Může jich být kolik chce, na konci se pak poskládají do jednoho…
Odpověď: To samosebou lze. Není ale režie „právě jedné“ instance menší než režie potřebná na poshánění spousty různých objektů dohromady?
Oponentura: Takže tam stále není NUTNOST onoho „právě jednou“, teoreticky jich může být víc…
Odpověď: Ne, NUTNOST tam je – z důvodů VHODNOSTI. Je jednodušší ohlídat, aby byl objekt jen jednou, než na konci hledat kde všude jaké jsou a vybírat, který z nich platí.
Oponuji si: Ale JE MOŽNÉ, aby si metoda vytvořila vlastní „druhý“ Response a do něj zapisovala, místo zapisování do globálně dostupného Response?
Odpověď: Ano, možné to je, ale jaký to má smysl? Na konci metody zanikne i tenhle bastardík a data zmizí – pokud tedy nebudou nějak propasírována do onoho „globálního“, nebo pokud nebude tahle instance kamsi uložena na „poslední zúčtování“.
Protiotázka: Ale stále chybí ta „NUTNOST“ té právě jedné instance.
Finální odpověď: Ber nutnost jako dobrovolné omezení. Stejně jako když píšeš metodu: Kontroluješ si předané hodnoty, i když víš, že tu metodu budeš volat zase ty sám ze svého kódu jinde, a „tam si to přeci ohlídáš“. Tak tady ber „právě jednu“ instanci jako opis pro „mohlo by jich být víc, ale byl by v tom maglajz, tak si pamatuj, že musíš udělat jen jednu“. No a návrhový vzor singleton není nic jiného než „soubor pravidel a postupů, jak sama sebe ohlídat před děláním zmatků“.
Jsem přesvědčen o tom, že tento komentář měl mít pointu, ale buď jsem ji zapomněl, nebo je v druhé instanci třídy „Martin Malý“ a dorazí na konci skriptu.
David Grudl #29
#28 Martin Malý, „Může jich být víc? Může, ale nemají smysl.“
To je klíčové. Zakazovat něco jen proto, že to momentálně nepotřebuju (nebo nevidím smysl), je řekněme … hloupé.
Zrovna v případě Response se může šiknout některou komponentu přesměrovat do speciální instance Response, která vše zahazuje (něco jako ob_start() … ob_end_clean(), zcela reálný požadavek), nebo přesměruje do souboru, nebo do testovacího objektu Response, který jen monitoruje, kdo jí co poslal a pomáhá hledat bug. Přitom to nic nemění na faktu, že existuje jeden objekt s výlučnou vlastností, že je to on, který bude na konci vypsán na HTTP výstup. A jako takový bude dobře dostupný přes globální identifikátor.
Vadu singletonu vidím v tom, že zavádí do aplikace omezení už na úrovni návrhu, odůvodněné tím, že momentálně mě nenapadá, k čemu budu za týden potřebovat víc objektů jedné třídy.
David Grudl #30
#28 Martin Malý, scrollbar mě zmátl a nedočetl jsem tvůj monodialog do konce.
ad Může jich být kolik chce, na konci se pak poskládají do jednoho… … Není ale režie „právě jedné“ instance menší než režie potřebná na poshánění spousty různých objektů dohromady? … Ne, NUTNOST tam je – z důvodů VHODNOSTI. Je jednodušší ohlídat, aby byl objekt jen jednou, než na konci hledat kde všude jaké jsou a vybírat, který z nich platí. – no jasně, že máš pravdu. Jenže oponuješ podivné konstrukci, která nesmyslně sbírá nějaké objekty – uvažujme spíš nad vhodně navrženou aplikací. Objekt není blbec a ví, že má psát do hlavního
Response::getInstance()->write()
, když chce psát do hlavního, nebo naopak bude psát do objektu, který mu někdo předal a pak to nemusí řešit. To se hodí třeba kvůli přesměrování výstupu, viz #29 David Grudl.Zkrátka tohle „dobrovolné“ omezení nevěstí nic dobrého. Dobrovolné samozřejmě v uvozovkách.
Martin Malý #31
#29 David Grudl, #30 David Grudl Aha, tak v tom je asi trochu zmateníčka zmatení… Moje RESPONSE není ekvivalentní output bufferu, nejdou do něj žádné HTML výstupy ani jiné. Jdou do něj jen data z controllerů. Pak se to, co Response posbírá, předhodí stroji, co to přechroupe, spojí se šablonami a pošle ven. Případné „tuto komponentu ne“ ošetřím na úrovni generování výstupního kódu nebo v místě, kde Response předává data, nikoli tak, že bych podstrkával různým komponentám různé Response instance.
Je jasné, že MOJE Response je v systému logicky jen jednou – ostatní Response nemají smysl. Pokud chci výstup do souboru, vytvořím si jinou třídu, která bude implementovat rozhraní IResponse, a její instanci podhodím konkrétním metodám. Ale stále to HTMLové Response zůstává jediné a samo. Nebo snad chceš říct, že tvá třída, která se stará o výstup HTML, zároveň implementuje ukládání do souboru? Pokud ano, tak pak je nesmysl navrhovat ji jako singleton, to je jasné.
Logika je ale evidentní: Pokud chci vypisovat do hlavního výstupu, použiju instanci singleton třídy Response. Pokud má být výstup přesměrován „libovolně“, tak předávám objekt, který implementuje rozhraní IResponse. Do toho komponenta zapíše a o nic víc se nestará. V systému mám tedy:
Po dobu vývoje mohu ad lib nastavit getInstance u třídy Response tak, aby mi vracel instanci TeeResponse. Po dokončení vrátím zpět na Response.
Ošetřit jedinečnost je dobré ve chvíli, kdy na tom dělá víc lidí, protože ti si nepřečtou dokumentaci a pak se budou divit, že si udělali new Response() a data fuč.
A teď mi řekni: Kdy přijde ta chvíle, že bych chtěl mít dvě instance třídy Response?
David Grudl #32
#31 Martin Malý, kdy přijde ta chvíle, kdy by existence druhé instance způsobila konec světa?
Martin Malý #33
#32 David Grudl, Aha, základní paradigmatický rozdíl v přístupu k progamování. Na jedné straně liberální, na druhé straně seberestriktivní.
Býval jsem liberálním programátorem, po čase (dlouhém) jsem přišel na to, že některá omezení, která si dobrovolně stanovím, mi ve svém důsledku pomohou dospět k cíli rychleji (nebo vůbec).
Řekl bych, že to je přesně tahle situace: Existence druhé instance třídy Response nezpůsobí konec světa. Ale já si navrhl systém, který počítá s tím, že má jen jeden objekt Response, protože víc Response instancí v téhle situaci nedává smysl. A když mám v návrhu to, že potřebuju jen jednu instanci, tak nemožnost vytvořit druhou mi pomůže vyhnout se hledání podivných chyb.
A asi HOWGH :)
David Grudl #34
#33 Martin Malý, to je trošku jinak, nezaměňujme příčinu a následek. Ty jsi aplikaci navrhl tak, že bude mít jednu instanci Response uloženou na nějakém globálně dostupném místě. Potud v pořádku.
Kdokoliv, kdo bude potřebovat s tímto objektem pracovat, ví odkud jej získat. Zcela v pořádku.
Teprve při prvním přístupu k tomuto přístupovému bodu se objekt vytvoří. Velmi správně.
Až sem se určitě shodneme. Také se určitě shodneme v tom, že existuje hromada způsobů, jak tuto logiku implementovat. Jeden z nich je popsán uplně na začátku článku a tvrdím, že zrovna tento je poněkud nešťastný, protože znemožňuje vytvořit více instancí – což je věc, která v zadání nebyla, jen jakýsi vedlejší efekt onoho řešení, pro který se zpětně snažíte dohledat argumenty. A tyto argumenty mi prostě nevoní.
Martin Malý #35
#34 David Grudl, Řeknu to, vzhledem k pozdní hodině, příměrem:
Při návrhu státu se vyšlo z toho, že bude jedna centrální banka. Ta bude vydávat peníze a udělovat licence a určovat úrokovou míru atd. To je jí dáno zákonem (v naší analogii „definice třídy“).
Podle tohoto zákona byla zřízena Česká Národní Banka (tedy „instance“).
A teď se zeptám tvými otázkami: Způsobilo by vytvoření druhé centrální banky, se stejnými pravomocemi a stejnými povinnostmi, „konec světa“? Je třeba znemožnit vznik druhé centrální banky? V zadání (ehm… ústavě) se s druhou centrální bankou nepočítá… Co by se stalo kdyby byla? Dvojí měna? A kdyby vydávaly obě stejnou?
Omlouvám se ekonomům… jen příměr…
A jinak, když to vezmu doslova – všiml sis, že argumentuješ kruhem? Titulní téma je singleton, tedy třída, která může mít jen jednu instanci. Tato vlastnost je nějak řešena. Ty onomu řešení vyčítáš to, že „znemožňuje vytvořit víc instancí – což je věc, která v zadání nebyla, jen jakýsi vedlejší efekt onoho řešení“.
Ne, to není vedlejší efekt, to je základ celé té věci a základ celého návrhu. Pokud je (dobrý) návrh takový, že vyžaduje, aby byla třída singleton, tak NEEXISTUJE žádný důvod, proč by se najednou mělo vytvořit víc instancí. A POKUD se takový důvod objeví, tak je třeba změnit návrh a nedělat třídu jako singleton – se všemi důsledky, které z toho vyplývají.
Jinak se ve zbytku naprosto shodujeme a jde tu pravděpodobně pouze o terminologický spor.
PS: Příčina a následek… Souvislost je tato: Pokud chci, aby něco mělo jen jednu instanci, tak to navrhnu jako singleton. Prosím, nesnaž se vytvořit dojem, že jediná instance je až vedlejší efekt singletonového návrhu, pro který se zpětně snažíme dohledat argumenty. Celý spor by se pak posunul do roviny dohadů nad tím, jestli někdy někdo má potřebu mít opravdu jedináčka, jestli to není chyba v návrhu a jestli to není jen zdroj problémů…
David Majda #36
Zajímavý článek k tématu na Google Testing Blogu: Where Have All the Singletons Gone?
(Vůbec, Miško Hevery teď na Google Testing Blog píše docela poučné články.)
Martin Malý #37
Prodloužená varianta mé ranní odpovědi je zde: http://www.misantrop.info/…e-kdepak.php
Martin Hassman #38
#21 David Grudl, No a já myslel, že singleton je v testování průmyslový leader. Podporuje řadu testů: statické, cyklické a mnohé další viz https://web.archive.org/…toncorp.com/
v6ak #39
Myslím, že když navrhuj rozhraní, tak skutečně nemůžu rozhodnout, že má smysl mít jen jednu instanci. Ale může to mít smysl ve chvíli, kdy to implementuji. Mám rozhraní SearchEngine a tady o Singletnou pochopitelně nemůže být řeč. Ale budu mít třídy Google, Yahoo, Jyxo apod. a tady se Singleton může hodit. (Teď je jedno, zda SearchEngine je klient nebo vlastní vyhledávač.)
Pak budu mít nějakou třídu, která bude pracovat s SearchEngine, třídu X. Tady použiju drátové spojení, Dependency Injection. Třídu X nezajímá ve výrazu new X(Google::getInstance()), že parametr je Singleton. Má prostě jiné zájmy.
Nebo čas: mám rozhraní Clock umožňující vrátit aktuální čas. Pak mám třídu (Singleton) SystemClock, která se dotazuje na systémový čas. Pak můžu mít různé časové mocky nebo třídy pro časový posun, které už nebudou Singleton.
V některých případech se může hodit Singleton, kde getInstance nevrací typ self, ale jiný. Třeba mám rozhraní Validator, pak mám třídu RegexValidator (validuje podle regulárního výrazu). Pak udělám třídu EmailValidator, která nebude implementovat Validator, ale bude mít getInstance vracející Validator. Ve skutečnosti to bude třeba RegexValidator se správným parametrem. Pochopitelně, třída RegexValidator by měla být neměnná, jinak mi někdo může EmailValidator „poškodit“.
Tento „Factory-Singleton“ může být i případ databáze. Něco takového ostatně používám. Neznemožňuje to ani Dependency Injection při správném použití.
Navíc „Factory-Singleton“ může být i řešením dědičnosti implementace Singletonové logiky. A mimochodem, s použitím drobného triku nemusíme čekat na php 5.3, možná bych to rozjel dokonce na php4 (ale nemám tu potřebu).
K třídě Response: není to sice vyloženě odstrašující příklad Singletonu, ale IMHO to není dobré. Nejen co se týče logiky. Napíšu si testy na Controllery, kterým podstrčím upravenou Response. Pak spustím všechny testy v jednom skriptu, abych otestoval Controllery celého webu. Tady už Singleton trošku pokulhává, ne?
To je snad vše, co jsem chtěl říct.
Bronislav Klucka #40
Ahoj,
v PHP opravdu nebyva singleton moc pouzitelny, ale podivejme se dale.
Treba takovy balancer musi byt prave jeden, pokud mate ridit pristup k vice zdrojum, potrebujete vedet jejich vyuziti, tzn. prohnat kazdy dotaz na zdroj jednim uzkym hrdlem, ktere umi rozhodovat, jaky zdroj se vyuzije. Nemit zadny je ocividne blbost a mit jich vice je blbost z hlediska logiky balanceru.
Dalsimi priklady mohou byt komponenty ridici vzhled aplikace (nastavim hodnotu, zmeni se vzhled cele aplikace), nastaveni programu, uzivatelska nastaveni, atd.
Navic Vas dotaz nepovazuji za uplne korektni. V programovani rozsahlejsich celku se dostanete do stavu, kdy neni mozne urcit jedinou spravnou cestu, ale spravnych cest je vice. Design patterns nejsou jedinou spravou cestou, ale navrhem pro reseni problemu.
U kazde komplexnejsi ulohy naleznete vice reseni. Vase otazka je jako ptat se, proc pouzit objekt na reseni uzivatele, vzdyt staci sada statickych funkci. Preci nemusim pouzit objekt, ale praxe ukazuje, ze je to nejlepsi reseni.
David Grudl #41
To je motání se v kruhu. Žádný z uvedených příkladu #40 Bronislav Klucka nenese známky singletonu: vždyť stačí objekt aplikace spojit s objektem řídícím vzhled nebo objektem balanceru. Ano, mohu do toho silou vklínit singleton, ale říkám, že je to zbytečné, ba co víc, není dobře.
Když bych to řekl ošklivě: singletony používají ti, kteří se ještě nenaučili psát bez nich 🙂
#36 David Majda, tu poslal výborný odkaz, zkuste zauvažovat na tímto + pokračováním. Alespoň si rozšířit obzor. Ten příklad s kreditní kartou je totiž geniální.
Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.