Dovolte mi malý experiment. Týká se všech programátorů, které baví návrh aplikací a OOP. Zadám vám velmi jednoduchý úkol, který má mnoho možných řešení. A spíš než konkrétní kód mě zajímá způsob uvažování. Budu rád, když se zapojí programátoři používající různé jazyky. Proto také zadání zapíši v pseudokódu.
Mějme třídu WebPage
, které zadáme URL a ona načte stránku
a vrátí jeji obsah, hlavičky a dokonce i náhled v podobě obrázku.
Příklad použití:
page = new WebPage
page.url = 'https://phpfashion.com'
echo page.url
echo page.body
echo page.headers
echo page.thumbnail
Chápejme url
, body
, headers
a
thumbnail
jako vlastnosti (properties, accessors) třídy
WebPage
. Je asi zřejmé z logiky věci a principu zapouzdření,
že zatímco url
umožňuje zápis i čtení, ostatní vlastnosti
lze pouze číst.
Protože funkčnost třídy WebPage
je výkonnostně i časově
náročná, je vhodné jednou získaná data ukládat do databáze. Jak to ale
implementovat? Sice nejsnadnější by bylo rovnou upravit kód třídy
WebPage
, jenže takový
postup je špinavý. Proč by třída s jednou srozumitelnou
funkcionalitou měla navíc ještě komunikovat s databází? Pověříme tím
tedy třídu WebPageStorage
. Ta nám bude, mimo jiné, schopna
dodat objekt WebPage
rovnou z databáze:
page = WebPageStorage.load('https://phpfashion.com')
echo page.url
echo page.body
echo page.headers
echo page.thumbnail
Otázka zní: jak to naprogramovat?
Zopakuji, že se pídíme po nejčistším řešení. Jak docílit toho, aby
metoda load
mohla vytvořit objekt a nastavit mu read-only
vlastnosti body
, headers
a thumbnail
na
hodnoty načtené z databáze? Vytvořením setterů nebo zpřístupněním
vnitřních proměnných by se porušil princip zapouzdření. Navrhli byste
redesign třídy WebPage
? Jaký? Navrhli byste redesign
WebPageStorage
? A co když úkol zkomplikujeme tím, že
thumbnail
se z databáze načte až při vyžádání?
Věřím tomu, že pro řadu čtenářů je zadání naprosto triviální. Přesto se zkuste zamyslet nad nejčistším řešením a vysvětlete jej v komentářích. Klidně obšírně. Díky!
Komentáře
Jod #1
Už som žul lakeť, kedy bude čo čítať :)
„a nad čím rozmýšlať“ :)
Jod #2
Napchať WebPageStorage do konštruktoru a vnútri ho spracovať? Tak sa s tým dajú páchať rôzne zločiny.
„Vyhral som niečo?“ :))) (jednu po hube)
Patrik Votoček (Vrtak-CZ) #3
Je
WebPageStorage
potomkemWebPage
?morousej #4
#3 Patriku Votočku (Vrtak-CZ), Podle mně ne – úložiště webových stránek není nějakou webovou stránkou, tudíž není jejím potomkem po straně návrhu.
Adams #5
A co dát metodě load další 3 volitelné parametry body, headers a thumbnail?
Washo #6
Napada me v konstruktoru WebPage akceptovat parametr, ktery nastavi parametry na hodnoty z databaze.
Washo #7
#6 Washo, … nastavi properties na hodnoty ulozene v databazi.
Sorry
Aleš Roubíček #8
WebPage
jako taková by neměla znát způsob načítání dat. Proto je vhodné zvolit dependency injection a konkrétníWebPageLoader
injektovat zvenčí. Pak už stačí udělat konkrétní implementace pro načítání z webu, z databáze a kompozitní implementaci, která se pokusí načíst z databáze a pokud tam nic není, načte z webu. K tomu použije předešlé dvě implementace. Vše je schováno třeba za rozhranní.Veškerá magie se zapouzdřením je pak v metodě
load
, kde se zavolá metodaWebPageLoader.load
, která může vracet DTO s načtenými daty, které potom nastavíme do lokálních položek.WebPageLoader
klidně může mít tři metodyloadContent
,loadHeaders
,loadThumbnail
, pak nebude třeba metodyWebPage.load
, ale můžeme je volat při přístupu na dannou vlastnost a máme tu i požadovaný lazzy loading.johno #9
Ten problem, ktory mam s tvojim pseudokodom je ten, ze WebPage bez url podla mna nema zmysel, tym padom, je po zavolani
page = new WebPage();
premenna v nejakom divnom, nedefinovanom nekonzistentnom stave.Riesim to tak, ze mam protected konstruktor, ktory len natiahne data z nejakej struktury do instancnych premennych. Potom mozem, vytvorit staticke create_metody, ktore ci uz data beru z db alebo priamo z webu, je uplne jedno. Trochu to zamotava, ten lazy loaded thumbnail, ale nieco som skusil. Je to ruby, ale nie je tam nic specificke, co by sa v php nedalo. Spustat som to neskusal, ide o ideu.
Co sa tyka statickych create_metod. Ovela lepsia finta by bola, keby bola volitelna polymorfia na zaklade typov v dynamickych jazykoch. Predstavoval by som si to nejako takto.
Aleš Roubíček #10
#8 Aleši Roubíčku, To, co jsem tu popsal, je defacto návrhový vzor Strategie
René Stein #11
Dostal jsem několikrát hlášku, že komentář je příliš dlouhý, tak jsem kód dal alespoň na clipboard.
Souhlasím z velké míry s tím co napsal Aleš – dokonce bych Aleši řekl, že jde o pěknou variaci na vzor Repository (Composite v pozadí), který v originále také používá dvě strategie – jednu pro nahrání objektů z paměti a druhou pro nahrání objektů z databáze.
Ve svém skeletonu řešení jsem zkusil jiný postup, který se víc drží původní myšlenky Davida a nepředpokládá tolik berličky typu DI kontajner.
Kód je komentovaný (C#), snad je z něj patrné, co dělá. šlo by ho samozřejmě vylepšit o automatické adaptéry pro lazy loading apod.
http://www.clipboard.cz/9hg
Martiner #12
Jak docílit toho, aby metoda load mohla vytvořit objekt a nastavit mu read-only vlastnosti body, headers a thumbnail na hodnoty načtené z databáze?
V Javě bych to řešil tak, že pro body, headers, thumbnail mám jen gettery.
Pro ukládání do databáze použiju Hibernate/JPA, který načtené hodnoty z databáze nevkládá přes settery, ale vloží je přímo do property.
A co když úkol zkomplikujeme tím, že thumbnail se z databáze načte až při vyžádání?
V případě Hibernate žádný problém – lazy property loading.
Tomáš Fejfar #13
Já bych šel cestou DI.
A uvnitř metody pro získání thumbnailu
Tomáš Fejfar #14
#13 Tomáši Fejfare, ještě vysvětlení, proč si myslím, že tohle řešení je nejčistší.
$this->_{$property} = $this->_storage->get{$property}();
Tomáš Fejfar #15
#14 Tomáši Fejfare, A ještě jedna věc, třídě WebPage_Storage se dá poslat celý objekt WebPage a ta si z něj vytahá nějaký PK podle vlastní logiky. Ale možná to je zbytečná komplikace. Myslím ale, že by pomohla rnovupoužitelnosti celého konceptu i na jiné věci než WebPage…
Martin Štěpař #16
Lze považovat použití funkcí
serialize
aunserialize
v PHP za čisté řešení?Jiří Knesl #17
Takže nejčistší řešení pro tuto (předpokládám) cache stránek bych viděl já následovně:
Proti tvému zadání jsem WebPageStorage rozdělil na WebPageFactory a WebPagePersistence, protože to jsou dvě odlišné (persistence a vytváření instance) zodpovědnosti. Snad to nevadí. :)
Obecně jsem hlavně vyšel z toho, že je možné oddělit od sebe proces vytváření instance, vnitřní strukturu objektu a jeho použití. Pak už bylo řešení triviální, ikdyž bylo potřeba celkem 6 tříd (což mi nevadí, malé třídy se stejně lépe testují a upravují).
Jiří Knesl #18
Zkusím to ještě radši shrnout:
5 tříd, 1 rozhraní, omlouvám se za špatné sčítání předtím.
Aleš Roubíček #19
#18 Jiří Knesle, Jo, takhle nějak vypadá overengineered solution :)
Jiří Knesl #20
#19 Aleši Roubíčku, No tak rozdělit persistenci a factory ti snad jako overengineering nepřijde.
Definovat si rozhraní tam, kde mám 2 třídy, které se mají chovat stejně, to je taky přirozené. Právě naopak by byla hrubá chyba rozhraní nemít.
Osekáno o toto:
Takže my dva máme 2 řešení – tebou preferované Strategy (která bude taky potřebovat nějakou nějakou logiku zvenčí – tedy taky nějaký objekt, který jsi nezmínil), mnou preferovaný Decorator (který už tu logiku má v té obalující třídě).
Ať se dívám, jak se dívám, přijdou mi naše řešení identicky složitá.
Toby77 #21
Použil bych Dekorátor. Třída WebPageCached by obalila třídu WebPage, navenek zprostředkovala všechny její metody, ale jejich výsledky by navíc cachovala.
Má to tu výhodu, že WebPage nemusí nic tušit o zápisu do DB a WebPage nic o stahování přes http.
David Majda #22
Předně, vlastnost
url
třídyWebPage
bych realizoval jako read-only a nastavoval ji už v konstruktoru, kde by se (nepřímo) realizovalo i parsování. TřídaWebPage
by v mém podání tedy byla bezstavová, což zjednodušuje její logiku, ale zas by neuměla zpracovat víc URL po sobě.Teď k problému. Do instance třídy
WebPage
je potřeba nějak dostat údaje o stránce. Volil bych cestu konstruktoru a tříděWebPage
přidal druhý konstruktor s parametryurl
,body
,headers
athumbnail
.Pokud by mi jazyk neumoňoval přidat druhý konstruktor (nebo používat jeden dvěma různými způsoby), tak bych zrušil původní konstruktor, přidaný kontruktor učinil privátní a použil dvě statické metody
WebPage::new(url, body, headers, thumbnail)
aWebPage::parse(url)
. Ty by uvnitř volaly onen privátní konstruktor aWebPage::parse
by navíc řešila i parsování.Pokud by se thumbnail měl načíst až na vyžádání, tak bych místo thumbnailu třídě
WebPage
předal callback, který by thumbnail na požádání vrátil. Konkrétní realizace callbacku záleží na použitém jazyce (např. předání uzávěru, nebo speciální objekt).Toby77 #23
#21 Toby77, Navrhuju zřejmě to samé, co Jiří Knesl, ale v tom jeho návrhu jsem se trochu ztratil. Rozhraní a fabriky neřeším, pro jednoduchej příklad mi to přijde zbytečné. Stejně tak lazy loading, kontrolu konzistence, atd. Že to půjde udělat, je evidentní.
David Majda #24
#8 Aleši Roubíčku, Něco podobného tvému řešení mě taky napadlo. V podstatě to znamená udělat z
WebPage
hloupý objekt odpovědný jen za držení dat o stránce, případně jejich líné načtení odněkud. Nakonec mi ale přišlo, že už by to bylo až moc velké rozmělnění odpovědnosti.BTW Davide moc hezká úloha – kdybych ještě učil na matfyzu, studenti by ji dost možná dostali zadanou na cvikách 🙂
rsvanda #25
WebPageStorage.load by neměla vracet přímo instanci WebPage ale proxy, která bude řešit ten lazy loading. Do ní konečně můžete předem setnout co budete chtít.
Inspiraci zkuste u hibernate.org.
ady #26
Co takhle:
page = new WebPage(‚https://phpfashion.com/‘)
a v constructoru WebPage:
WebPageStorage wps = new WebPageStorage(‚https://phpfashion.com/‘);
a nasetovat WebPage z WebPageStorage objektu
v constructoru WebPageStorage:
zkusit loadnout z DB a pokud neni tak nasetovat/nebo jinak zkonstruovat, save do db a vratit?
Ale jsem lamer, takze mozna vsechno spatne :)
ady #27
#26 ady, A sakra koukam, ze vse jiz bylo receno – mel jsem stranku na pozadi a nenapadlo me to reloadnout pred psanim komentare :)
paranoiq #28
hezký příklad. přiznám se, že většinou čistě řešení nechávám na později
„Mějme třídu WebPage, které zadáme URL a ona načte stránku“ – pokud má být kešovatelná, neměla by řešit, odkud se data berou. tudíž zodpovědnost za načtení dat bych jí sebral
zaujala mě řešení #8 Aleš Roubíček s DI a #17 Jiří Knesl s factory a proxy. zdají se být stejná, ale nejsou
půjdu od lesa. vytvoření náhledu je také velmi náročná operace a náhled je samozřejmě nutné kešovat. vytvoření náhledu přitom spadá do kompetence třídy WebPage. jen ta ví jak se má vykreslit. na počátku WebPage náhled nemá a tudíž si ho při prvním požadavku vytvoří (může to za ní udělat nějaký jiný objekt, ale určitě ne úložiště). pak by jej samozřejmě ráda uložila. pokud ji však na svět vyplivla nějaká factory, třída nemá vazbu na úložiště. tady příklad s factory selhává. pokud jí tuto vazbu dodáme, jsme zase u DI. je tedy lepší použít dependency injection rovnou
…
paranoiq #29
výsledek:
• WebPage – umí vygenerovat náhled. data dostává od WebPageDataSource a případně mu posílá náhled. DS dostává konstruktorem, nebo si jej bere líně třeba od nějakého globálního service providera
• WebPageProvider – žádá o data své adaptéry. předává data úložišti
• WebPageStorage – umí uložit a vrátit data a náhled
• WebPageLoader – umí stáhnout data
DTO je u takhle jednoduchého příkladu asi zbytečný. pokud by bylo dat víc, tak by se určitě uplatnil taky
BTW: nechci ti do toho kecat Davide, ale když kladeš takovéhle otázky, měl bys s dlouhými komentáři počítat :P
Jod #30
Ja by som to spravil takto:
Pastie: https://web.archive.org/…rg:80/673943
Example: https://web.archive.org/…omcok.eu:80/
(stačí vyplniť twitter nick do políčka a načítať, prípadne ešte uložiť do db. Z db to ťahá vtedy keď tam neni save button :P)
SVN example: svn export svn://home.romcok.eu/webpage-example/trunk
(databázu by to malo importnúť samo, ale šlo to len z 50%, riešenie na rýchlo)
Určite to má svoje chyby, ale ponáhlam sa preč :))
Tomas Petricek #31
Jen tak pro zabavu, aby tu taky zaznelo reseni z uplne jineho sveta 🙂, tak ukazka toho, jak by se k reseni pristupovalo (uplne nejjednodussim zpusobem) pri pouziti funkcionalnich jazyku (e.g. F#). WebPage v tve ukazce vlastne plni dve funkce – reprezentace dat a nacitani informaci o strance z webu. Otazka je, jak do takoveto implementace pridat nacitani dat z cahce v DB. To jde samozrejme spatne, protoze ty dve funkce jsou svazane v jedne tride. Nejjdodussi je napsat si nejakou datovou strukturu na reprezentaci dat:
type WebPage = { url : string; body : string; thumbnail : Bitmap }
…a pak dve funkce, ktere data nactou z ruznych zdroju podle dane adresy – napriklad
loadFromWeb
aloadFromDB
.Abych si trosku rypnul – musim rict, ze me obcas prekvapuje jak silene slozita reseni se daji v modernich objektovych systemech vymyslet, na reseni tak trivialniho problemu 🙂.
Tomik #32
Ve zkratce:
(možná, že se to schoduje s nějakým výše zmíněným postupem, nevím, jen jsem to prolétl, tak se omlouvám…)
WebPage i WebPageCached by parsovaly už v kostruktoru, resp. volaly by odpovídající metody.
K tomu stížení: pokud by byl záznam v DB, tak si v třídě WebPageCached pouze někam uložím příznak, že náhled existuje a načtu jej až lazy v getteru.
paranoiq #33
v c++ by se tuším na naplnění interních parametrů dala použít nějaká
friend
třída. není to něco podobného jako právě přidanýinternal access modifier
z Nette?Aleš Roubíček #34
#33 paranoiqu, Jenže
internal
nebofriend
umožňuje přístup pouze těm, co jsou ze stejného balíčku (assembly). Pokud chci mít možnost přidávat další (externí) způsoby nahrávání je tento přístup nedostatečný.paranoiq #35
#34 Aleši Roubíčku, to ano. ale nikde není řečeno, že přístup musí být i z jiného balíčku (pak by byla na místě DI). mě se zdá, že právě k podpoře klíčového slova friend David směřuje. ale třeba je to jen můj dojem. rozhodně by se mi takové směřování líbilo :]
Aleš Roubíček #36
#35 paranoiqu, Ano, na takové věci se přichází až později. :) Pak se to musí přepsat… Lepší je se držet Open-Closed principu už od začátku. :)
Jakub Vrána #37
Viz moje řešení.
paranoiq #38
#36 Aleši Roubíčku, musím přiznat, že nemám tušení v čem open/closed princip spočívá. nastuduji si.
balíčkování platí pouze pro internal, nikoliv pro friend. ideální by IMO bylo přiřadit té soukromé vlastnosti či metodě nějaký friend interface, což ale nejde ani v tom c++ (šlo by to ale zařídit třebas v php)
Borek #39
Moc hezký experiment, který ukazuje, jak kdo přemýšlí. Trošku mě překvapuje, že jsem zatím neviděl řešení, které mi připadne přímočaré a ve světě .NETu snad i evidentní:
WebPage
aWebPageStorage
bych dal do projektu „knihovna“ (a když už bych byl u toho, přejmenoval bych je naWebPageInfo
aWebPageLoader
, které podle mého názoru lépe vystihují, co představují)WebPageLoader
by měl public metoduLoad(url)
, která by řešila cachování / stahování.WebPageInfo
by na všech properties měla public getter a internal setter. To znamená, že z metodyWebPageLoader.Load()
můžu tyto vlastnosti nastavit, zatímco v aplikačním kódu se budou všechny vlastnosti tvářit jako read-only.Základním trikem jsou tedy dva projekty a potom public gettery / internal settery. V jazyku, který „internal“ nebo podobný koncept nepodporuje, bych patrně volil runtime exception, protože mi ostatní řešení připadají ještě ošklivější.
Zajímavou otázkou je, proč je v komentářích k vidění tolik komplikovaných řešení (často asi zbytečně komplikovaných). Možná je to zadáním – ona je totiž třída WebPage, tak, jak je prezentována v první ukázce, trochu podivná, protože představuje současně webovou stránku a její downloader. Z pohledu OOP je to podle mého názoru špatná abstrakce, což je naštěstí vzápětí vylepšen ukázkou s WebPageStorage, ale zdá se mi, že utkvělá představa udělala v řadě případů své.
Aleš Roubíček #40
#39 Borku, Možná jsem své řešení komplikovaně popsal, ale rozhodně implementačne komplikované není. :) BTW
internal
je takový cheat proti zapouzdření, v tomto scénáři to není čisté řešení. ;)PHX #41
Osobně bych šel touto cestou:
WebPage
ciWebPageStorage
.myPage.page
by ìnterně směrovala naWebPage
nebo by data načetla z cache (DB) a gettery by interně směrovala naWebPageStorage
.René Stein #42
#39 Borku, Borek:
optik #43
#38 paranoiqu,
přesně ta „nepodpora“ C++ friend, v PHP mě napadla jako první, prostě nějak zajistit, aby WebPage mohl naplnit jen někdo, kdo není v jedné hierachi s ním. Zdá se mi, že navržená řešení se to snaží simulovat přes různé dekorátory a proxy.
René Stein #44
A ještě pro všechny fanoušky špatně strávených dekorátorů a proxy, kteří se tu přemnožili. 😁
Možná bych si jako první věc uvědomil, že za dekorátora se považuje hlavně třída se stejným rozhraním jaké má dekorovaný objekt.
Že u vzoru proxy by si klient neměl být vědom odlišnosti proxy od reálného subjektu.
A zkuste si porovnat, jak má vypadat třída WebPage a jaké to bude mít důsledky (v různých jazycích různé, já vím) – přetypovávání apod.
Borek #45
#40 Aleši Roubíčku, Úplně souhlasím, tvé řešení komplikované není a technicky se mi moc líbí, jen implementuje API, které je podle mého názoru špatnou abstrakcí (kombinuje objekt reprezentující stránku a objekt načítače stránky). Takže po technické stránce nemám námitek :)
Aleš Roubíček #46
#45 Borku, Mě se ta kombinace popravdě taky moc nelíbí, ale když to tak bylo v zadání. ;) Proto jsem samotný akt načtení raději abstrahoval za injektovanou strategii. :)
René Stein #47
#45 Borku, Raději bych se zcela vyhnul hodnocení „špatná“ a „dobrá“ abstrakce. Abstrakce, stejně jako celé modely, knihovny a frameworky se dají rozdělit na dobře nebo špatně použitelné – a mezi těmito extrémy je celá škála přechpdů . Žádné exaktní kritérium pro ohodnocení míry dobra abstrakce neexistuje, jen (mnohdy špatně chápaná) vodítka typu SRP. Spojení „dobrá abstrakce“ beru jako univerzální hůl, kterým se dá bít návrh libovolné třídy (možná kromě třídy s jedinou odpovědností vyjádřenou metodou DoIt :))
Aleš Roubíček #48
#47 René Steine, A i ta by mohla být pouze funkcí. :)
Borek #49
#46 Aleši Roubíčku, Já zadání vyčetl takhle: potřebuješ nějakou tovární metodou vytvořit objekt X a nastavit mu vlastnosti A, B a C. Jak to uděláš, když se ti dvakrát nelíbí, že by A, B a C měly mít public settery? (René to řeší tak, že všechny vlastnosti má jako parametry konstruktoru, já radši „internal“).
Ale je fakt, že úvod o třídě WebPage je hodně matoucí, takže kdo ví, jak přesně to vlastně celé bylo myšleno :)
Borek #50
René: Vaše řešení se nabízí, jen mě napadlo, jak budete řešit situaci, když bude mít třída WebPage vlastností dvacet?
Jinak rovněž rád uvidím, jak v dynamickém jazyku vaší volby zařídíte, aby byl getter public a setter z venku neviditelný. Souhlasím s vámi, že se zde bavíme na obecné úrovni, kde techniky z .NETu nemusí fungovat, proto bych taky rád slyšel dovysvětlení toho, jak jste to myslel s tím kompilátorem (jakým?).
Borek #51
#47 René Steine, Ano, návrh API je umění a do značné míry subjektivní. Můžete ale odkázat na nějaký framework, kde by se „stahovač“ a jeho „výsledek“ reprezentovali jako jedna třída? V .NETu, Flexu i Javě tomu tak není.
Aleš Roubíček #52
#51 Borku, Já jsem již včera chtěl napsat: „jak to dělá Firefox?“ Protože tohle je jasná implementace načítání stránky v browseru včetně cache.
René Stein #53
#50 Borku, Ano, to je to kdyby. Vyšel jsem z Davidova zadání – úprav a vylepšení si mohu vymyslet spoustu.
Když bude vlastností 20, použiju jiné řešení. :)
Ale i tady – pro předání dat něco jako DTO objekt („krabička na data“). V některých situacích stačí i IDictionary (hashovací tabulka s init hodnotami pro objekt). Pro lazy inicializaci mohu předat sadu funkcí místo hodnot apod. a samozřejmě společnou logiku pro lazy inicializaci zapouzdřit do předka pro celou vrstvu A/nebo použít AOP.
U výjimky jsem narážel např. na C++, které viditelnost na úrovni modifkátoru internal v C# nemá (i kdyč jdou použít jiné triky). Dynamické jazyky mají svá specifika, ve statickém jazyce je samozřejmě velký rozdíl mezi vyvoláním výjimky v set akcesoru za běhu aplikace a neexistencí set akcesoru při kompilaci.
René Stein #54
#51 Borku, Na co narážíte? Na nejkřiklavější analogii s tímto příkladem v .NET, kterým asi je WebRequest, WebResponse a ResponseStream?
Třída v sobě může skrýt, že interně používá WebRequest apod.
Co třeba XmlTextReader?
XmlTextReader reader = null;
reader = new XmlTextReader(url or filename);
XmlTextReader má konstruktor, který říká – umím stáhnout data a pak na mě klidně volej metodu Read pro čtení výsledného XML.
A samozřejmě existuje i „dirty“ metoda Create na XmlReaderu.
Borek #55
#53 René Steine, To řešení pro 20 vlastností by mě docela zajímalo – šlo by naznačit aspoň v pseudokódu?
Jinak problém vašeho původního řešení vidím v tom, že vytvoření instance WebPage je ve vašem API 2× (jednou přes konstruktor, podruhé přes factory). Z pohledu klientského kódu mi nebude na první pohled jasné, které z nich použít (případně kdy a proč).
Borek #56
René: Ještě se vrátím k tomu, jak vám připadá úsměvné dělení do assembly jen kvůli dosažení žádané viditelnosti.
V .NETu nejsem žádný expert, nicméně pokud je zadání to, že WebPageLoader má vrátit instanci WebPage, jejíž vlastnosti se mají ke klientskému kódu tvářit jako read-only, jak to realizujete? Ve vašem kódu sice nelze vlastnosti na WebPage měnit, nicméně stále lze vytvářet nové instance s libovolnými vlastnostmi, což patrně není velký problém, ale zbytečně to do API přidává něco, co tam být nemusí. Je nějaká jiná možnost, jak se toho zbavit, kromě
internal
?(Předchozí samozřejmě platí v mém chápání zadání, které se nemusí shodovat s vaším. Jen pátrám po tom, co vám na internal v tomto kontextu připadá tak úsměvné.)
Rene Stein #57
#55 Borku, Ad 20 vlastnosti – DTO objekt znate dobre Borku, ne? Proste jednoducha struktura s get a set akcesory, ktera mi umozni predat do konstruktoru misto 20 argumentu jeden jediny. Podobne jako kdyz dotahuji DTO z db/servisni vrstvy.
Ad dvoji API – to jsem tam nechal kvuli puvodnimu zadani, nikdo vam nebrani, abyste i posledni verejny konstruktor skryl a donutil klienta pouzivat jen factory metody.
Mimochodem, jiz mnou zminovany XmlReader ma metodu Create (dokonce nekolik variant) a trida XmlTextReader ma sve specificke konstruktory.
(Psano z mobilu)
René Stein #58
#56 Borku, Ta usmevnost se tykala ne internal clenu jako takovych, ale faktu, ze vetsinou musite resit pri navrhu a rozdeleni aplikace do jednotlivych assembly tolik problemu, ze delat separatni knihovnu kvuli internal clenum je luxus. Ano – pokud maji tridy spolecne rysy a logicky patri do stejne assembly, tak je vse v poradku.
Konstruktor je misto, ktere overi, ze predane hodnoty do konstruktoru splnuji vsechny pozadavky na vytvoreni legalni-platne instance. Pokud ano, tak nevidim problem v pouziti konstruktoru/metod klientem. Ze staticke cleny udelate v . net internal, pokud to lze, a mate je prirozene a nenasilne v jedne assembly? Proc ne.
Ale kdyz se ptate na jine reseni bez internal clenu, zde je jedna moznost(moc se mi nelibi, tesne vazby mezi tridami, je prekombinovane a ma i dalsi nevyhody).
Vhodnejsi by bylo pri inicializaci modulu zaregistrovat instanci WebPageLoaderInner v repozitari sluzeb jako implementora rozhrani IWebPageLoader. Rozhrani IwebPageLoader muze byt dostupne pres „Extension Interface“ atd.
Privatni trida WebPageLoaderInner ma pristup ke vsemu potrebnemu v obalujici tride WebPage, vlastnosti jsou stale pouze pro cteni a nemam internal cleny. Prred klientem jsou vsechny slozitosti skryty. Jen jedna ukazka toho, na co jste se ptal…
A pres repozitar sluzeb se pomalu jinou stezkou vracime k DI, kterou jsme zminovali na pocatku s Alesem.:)
(Psano na mobilu)
BTW: Indiana Jones tady radi fakt drsne. Nejdriv mě upozornil, ze muj dlouhy komentar nikdo cist nebude a pak jej rovnou poslal do pr… (skvela napodoba kultovni sceny v prvnim dile ?)
Borek #59
#57 Rene Steine, DTO znamená, že se ve vašem API objeví další třída, která bude co se týče vlastností velmi podobná WebPage. Rovněž si nejsem úplně jistý, že #58 René Stein řeší problém, který jsem popsal v #56 Borek.
Předně ale myslím, že oba vnímáme zadání trošku jinak, takže se těžko na všem shodneme. Každopádně díky za vhled do toho, jak byste problém řešil vy. Třeba David někdy upřesní, jak to všechno myslel :)
Rene Stein #60
#59 Borku, DTO v API byt nemusi. Uvadel jsem nekde vyse i objekt Dictionary. Problem, resp. situaci, kterou jste popsal IMO muj postup resi (se vsemi vyhradami, ktere u nej jsou).
Take Borku diky za podnety. Myslim, ze v poslednich prispevcich jsme u spekulaci a hricek „kdyby“, ktere uz se Davidova textu moc netykaji. Logout:)
Karel Minařík #61
Tak já se přiznám, že jsem z celé té debaty v těch „robustních“ jazycích a cvičení s návrhovými vzory dost zpitomělý, nicméně zkusil jsem si to napsat v Ruby (cca 1hod), a výsledek je zde:
→ http://gist.github.com/222260
Mám jednu nefér výhodu – nedávno jsem dělal generické kešování do RubyGemu HTTParty [https://web.archive.org/…for-httparty]
Dovolil jsem si přitom změnit zadání, které mi nedávalo smysl – nikde není zdůvodněno, proč potřebuji nějakou třídu
WebPageStorage
, která jaksi „kešovaně“ obalujeWebPage
.Já chci mít normální WebPage, a ta si případně nějak pořeší cachování, které skryje za svoje API, které může být v tomto případě zcela triviální:
Crawler::WebPage.load('htpp://example.com')
Ve skutečném životě by samozřejmě bylo třeba:
Každopádně díky za užitečný experiment!, bylo by úžasné někde na Githubu udělat repositář, kde by šly skladovat různé implementace v různých jazycích/platformách.
A pro mně osobně z toho plyne, panebože, zaplať (Matzovi) za to Ruby. Zaplať tisíckrát.
Borek #62
#61 Karle Minaříku, Zastav si myš nad „takový postup je špinavý“ :)
Karel Minařík #63
#62 Borku, Nerozumím. To zadání už nějak předpokládá implementaci, která mi přijde od začátku špatně.
Davidovo vysvětlení:
mi navíc nic okolo toho nevysvětluje. Třída WebPage samozřejmě nijak „nekomunikuje“ s databází. A nechápu, proč by pak mohla WebPageStorage „komunikovat“ s databází. Protože se tak „jmenuje“? :D
Rozdělení zodpovědností mezi zpracování HTTP requestu a cachování, to je to, co je tady zcela zásadní. A prvním svodem je tu samozřejmě dědičnost [http://www.artima.com/…ciples4.html].
No. Pokud je hra o tom „tady máš třídu a tu nesmíš změnit ale nějak se to musí cachovat“ – ještě, že nemusím takováhle „klimatizovaná a skleněná“ zadání moc řešit – , tak jsem nepochopil, čím se na tom zabýváme: tak se udělá nějaké to
CachedWebPage.load(...)
, které buď vytáhne objekt z cache — a nebavme se prosím pořád o „databázi“ — nebo vrátí WebPage.load(…) a zároveň ho nacachuje. Tak jak je to i v té mojí implementaci. Atd. Atd.No nic. Bože, ochraňuj mně prosím dál před .NETem, PHP a tímhle vším :)
Honza Kral #64
Ma troska do mlyna
Idea je jednoducha – WebPage je jen lazy wrapper kolem nejakeho objektu, ktery je uplne tupy a je poskytovan nejakym loaderem, defaultne je nastaven tupy loader ktery to stahne, zparsuje a zpristupni na sobe jako atributy (nedelal jsem read-only, to necham jako domaci cviceni). Existuje i cachovana implementace na ukazku.
Vyhoda Lazy reseni je, ze narocne zpracovani (stahovani, parsovani) lze odlozit az do doby kdy nekdo skutecne ty data chce.
Aleš Roubíček #65
#61 Karle Minaříku, Můj komentář vycházel z implementace, kterou jsem si zkusil za pomocí TDD a Davidova textu vytvořit. Dám to nějak o víkendu dokupy a nahodim na github ;)
Borek #66
#63 Karle Minaříku, Já proti tvému příkladu nic nemám, jen neřeší problém z článku. (Ten byl: máš třídu Loader, která vrací objekt WebPage. Loader na tomto objektu musí nastavit nějaké vlastnosti, ty se ale potom ke klientskému kódu mají tvářit jako read-only.)
Můžeme se dohadovat, jestli se nám tento návrh líbí nebo ne, ale jak David explicitně píše v článku, pokud chceš funkčnost realizovat jednou třídou, nejsi na tento experiment vhodný adept. Když totiž všechno zapouzdřím v jediné třídě, problém pochopitelně odpadá jak v .NETu, tak v PHP i v dalších jazycích, které rozlišují public/private a jiné modifikátoru přístupu. Jinými slovy, kdyby chtěl David funcionalitu realizovat jedinou třídou, tento článek by patrně vůbec nevznikl.
To je celé.
Honza Kral #67
Jeste detail – priklad je skutecne spatne zadan – text a myslenka zadani jiz pocita s nejakou implementaci, kdyz to prezenu tak zadani zni za predpokladu ze to udelate takhle, jak to udelate?
Ja bych to skutecne resil takhle jednou tridou, ktera vyuziva jinou jako backend. Je to flexibilni (muzu mit externi backendy ktere vubec neznaji muj kod) a velmi jednoduche na implementaci. Snadno bych mohl vytvorit konfiguraci ktera mi bude specifikovat defaultni loader a podobne kejkle.
Karel Minařík #68
#65 Aleši Roubíčku, Počkat, počkat, jaký komentář – myslíš ten svůj původní o „řešení“ někde úplně nahoře nebo se něco ztratilo?
#62 Borek Něco takovéhohle by „jakoby“ bylo to „čisté řešení“ nebo ne? .)
module LostInTranslation
class WebPageStorage
def self.load(url)
return cache.get( url ) if cache.exist?( url ) # Already cached
webpage = Crawler::WebPage.load( url )
cache.set(url.to_s, webpage) # Cache the result
return webpage
end
end
end
→ https://gist.github.com/…8ac18a0f6c69
(Nevím, jak utvořit správné
<pre>
)Aleš Roubíček #69
#68 Karle Minaříku, Neztratilo, myslel jsem #8 Aleš Roubíček. Což je BTW stejne řešení jako #64 Honza Kral. Jen já říkám, že jde o vzor Strategie a Dependency Injection, a Honza to má za backendy. :) U mne je implementace složitější o rozhraní, která v Pythonu nejsou.
Karel Minařík #70
#66 Borku, Ale ne, kdepak jednou třídou. Cachování i reprezentace HTML stránky musí být oddělené a taky jsou. Můžeme se tedy bavit o tom, zda-li má
WebPage
vůbec nějak vědět o cachi nebo ne. Pokud ne, viz „architektura“ v LostInTranslation výše.(Navíc, v zadání o nějaké třídě
Loader
nebyla řeč – jen oWebPage
aWebPageStorage
.)Já se nijak nebouřím proti tomu, dělat jakékoliv kejkle (byť jsem jen ze čtení toho kódu docela zoufalý), ale tvrdím, že:
WebPage
nemá mít možnost vědět o cache, tak si můžeme pohrát s design patterns a vymýšlet, jak by se celé to API dalo zkomplikovat. Viz LostInTranslation.)WebPageStorage.load(...)
zní a vypadá naprosto strašlivě.WebPage.load(..., :cache => true)
mi zní rozumněji.Borek #71
#70 Karle Minaříku, Než budu odpovídat dál, rozlišujete v Ruby public/private/protected? Nějak si totiž nejsem jistý, jestli jsme na stejné vlně.
Karel Minařík #72
#71 Borku, Ano, rozlišujeme, ale odpovídej radši na svém blogu/Twitteru/Facebooku, ať to tady nezanášíme detaily…
Borek #73
#72 Karle Minaříku, Naopak, nerad bych touhle debatou zanášel další stránky na malém českém internetu :)
Jen stručně: v tvém LostInTranslation, které už odpovídá struktuře ze zadání, máš dva způsoby, jak vytvořit instanci WebPage – přes
WebPageStorage.load()
(BTW, taky se mi to jméno nelíbí) a přesWebPage.load()
(doufám, že obě tyto metody jsou public; nějak mi to ze zdrojáku není zřejmé). Z pohledu pak uživatele není jasné, proč bych měl použít jedno nebo druhé, což je problém API, ale to asi nemusím dvakrát vysvětlovat.No a jestli teď použiješ argument, že právě proto bys návrh změnil, tak se zacyklíme :) Jen dodám, že návrh použitý Davidem je běžný a divil bych se, že by v Ruby / Rails žádná funkcionalita nebyla navržená podobně.
Almad #74
Ze zadání a z odpovědí, rozumím tomu dobře, že se jedná o cvičení v návrhu aplikace, kde se nesmí měnit návrh tříd a jde o to udělat co nejlepší návrh?
Karel Minařík #75
#73 Borku, Jamis Buck, LEGOs, Play-Doh, and Programming:
veena #76
Ah, prokoukl jsem to! V contextu tohoto je to jasné.
Wait for it…
David si hledá nový programovací jazyk a nechal si udělat předváděčku zdarma ;-p
v6ak #77
Cache přes Decorator nebo Adapter. Já bývám spíše pro Decorator.
Design třídy WebPage se mi nelíbí. Byl bych pro továrnu (WebPageLoader) s metodou load, která vytvoří neměnnou (immutable) WebPage. Případně může pro složitější případy mít metodu createRequestBuilder().
Měl bych tedy více implementací WebPageLoaderu. Jedna by byla pro přímé vyslání požadavku (můžeme tu ještě uvažovat o podpoře proxy), druhá by byla pro použití cache. (Třetí a další nevylučuji.) Pomocí Dependency Injection by CachedWebPageLoader získal jak WebPageLoader používaný v případě nenalezení v cache, tak cachovací úložiště.
Nevím, co je složitého na lazy loadingu thumbnailu. Jen by se to mělo projevit do kontraktu (a v případě řízených výjimek i do nich) v throws.
Předání parametrů třídě WebPage (rozhraní by zde IMHO bylo příliš a zbytečně benevolentní) bych viděl na konstruktor. Omezení je zde při přidávání nových položek do WebPage, i když menší než u rozhraní.
Jake Cooney #78
Dal jsem public, ale jinak by tam byly nastavený gettery.
v6ak #79
#77 v6aku, Trošku jsem pročetl komentáře a všiml jsem si, že jsem zapomněl na to, že nanačtení thumbnailu se musí použít strategie. Při návrhu jsem na to sice zapomněl, ale při implementaci by to snad ani jinak přirozeně nešlo.
David Grudl #80
Děkuji všem za účast na experimentu!
Asi je čas prozradit, co jsem tím vším sledoval: chtěl jsem si ověřit domněnku, že programátoři odkojení na statických jazycích vysypou z rukávu řešení dle návrhových vzorů (overengineered way?), programátoři odkojení na dynamických jazycích přijdou s minimalistickým kódem (bastled way?) a programátoři v PHP budou na obě skupiny nechápavě zírat a ptát se, co je to interface, co je to zapouzdření a nakonec vyřeší úlohu pomocí serialize, ereg_replace a nebude to fungovat ;)
(ano, předchozí odstavec obsahuje prvky nadsázky)
Samozřejmě z komentářů nelze nic zásadního vyvozovat, snad jen to, že programátoři rádi uvažují ve svých jazycích. Takže pokud máte chuť rozšířit své schopnosti, potom je lepší, než hlouběji pronikat do tajů svého jazyka, naučit se jazyk jiný, nejlépe hodně jiný.
Almad #81
#80 Davide Grudle, @DavidGrudl: Mám pocit, že to je známá doporučená rada – každý rok jeden nový jazyk :)
v6ak #82
Já teď dělám hlavně Javu a myslím, že to je poznat. A to i přesto, že již v PHP jsem začal jakoby řešit „řízené“ výjimky a OOP. Java mi pak díky tomu pěkně sedla.
Zajímavé je, že když jsem se vrátil po delší době k PHP, tak jsem mimo jiné začal psát trošku jiným stylem než v Javě.
Další zajímavost je, že v době, kdy jsem v PHP nedělal, jsem si jej nechával na bastlení. Třeba jsem potřeboval zbastlit v co nejkratším čase skript, který vygeneruje „skript“ pro A2uploader na nahrání adresáře. („Skript“ pro A2uploader neumí ani if ani goto ani funkce, je to prostě rovná imperativní nudle.) Použil jsem PHP, asi protože v Javě bych tíhnul k čistějším řešením, což by mě vzdálilo od úkolu „dokončit v nějaké podobě co nejdřív bez ohledu na budoucí udržitelnost“.
S těmi jazyky souhlasím. Vždyť „Kolik jazyků znáš, tolikrát jsi programátorem!“
amsys #83
Čisté, co je v tomto případě čisté? Jediné co mě teďka napadá je vylepšit kontruktor, v případě, že existuje nepovinný argument tak mu budeme věřit a přednastavíme jednorázově hodnoty tz.:
P.S.: Nebo strávíme hodiny nad nějakým jiným řešením a za měsíc nebudeme mít na nájem, nebo se pletu?
Ondra #84
Jednoznačně to chce úpravu WebPageLoader, může fungovat jako proxy, a lazy loading obstará Hibernate – až při prvním požadavku transparentně načte obrázek z db .
Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.