Abstrakce nad abstrakcí

Školení, která pořádám

Zapojil jsem se do vývoje jednoho webu postaveného na Nette. Nad některými změnami, které by mi obvykle zabraly minutu, jsem strávil třeba hodinu. Aplikace je podle mě udělaná dobře, logické celky jsou oddělené, kód se neopakuje, takže v tom to není. Problém je, že kvůli několika úrovním abstrakce musím ty abstrakce nejdřív pochopit a naučit se, jak se v nich dělá to, co potřebuji. Přesně vím, co chci udělat, i jak se to ve finále udělá, ale oříšek je to přes ty abstrakce protlačit.

Příkaz po připojení k databázi

Kupříkladu jsem chtěl po připojení k databázi provést nějaký SQL příkaz. Ve starých dobrých časech jsem si ze skriptů includoval connect.inc.php, ve kterém jsem zavolal mysql_connect, za který bych si přidal to mysql_query, které jsem potřeboval. V této aplikaci je do prezenteru injectován repozitář – o to se asi nějak stará samo Nette, fajn. Repozitáře mají rodičovskou třídu, do jejího konstruktoru jsem tedy zkusil doplnit volání toho příkazu, což ale samozřejmě vedlo k tomu, že jednotlivé repozitáře příkaz volaly nezávisle na sobě a ten se tak provedl opakovaně. Konstruktor repozitářů přijímá instanci Dibi. Kde ta se bere? Mohl bych příkaz doplnit tam. Ale v kódu se přímo nikde nevytváří, asi ji zase injectuje Nette. Přímo do existujícího kódu si tedy příkaz nikam nedoplním, musím nějak přesvědčit Nette nebo Dibi, aby to udělaly za mě. Říkám si, že asi nejsem první, kdo něco takového v Dibi potřebuje. Koukám se do dokumentace, ale tam o tom nic nenacházím. V API dokumentaci projíždím seznam metod a taky tam nic nevidím. Nakonec se koukám do zdrojáku a zjišťuji, že to Dibi skutečně podporuje. Ono to je nakonec i v té API dokumentaci, ale ne moc přehledně:

Ukázka dokumentace

Pak už na první dobrou hádám, že stačí tenhle parametr přidat do sekce dibi v config.neon (syntaxe NEON je další abstrakce, ale naštěstí jednoduchá) a kód skutečně funguje.

Co když by Dibi něco takového nepodporovalo? Jsem jeden z těch, kdo by poslal pull request, ale co když by ho autor nepřijal? Tuším, že i to by se dalo vyřešit. V Nette bych si jistě mohl udělat nějakou DibiFactory, která by ten příkaz zavolala tam. Ale je to další abstrakce, kterou bych si musel nejdřív nastudovat.

Doplňování jména

V jednom formuláři je políčko name, které značí název místa, a políčko email, do kterého se zadává e-mail člověka, který název zadává. Chrome (a asi i další prohlížeče) při vyplňování name nabízí jméno člověka, což samo o sobě moc nevadí. Co je horší, že při automatickém doplnění e-mailu se přepíše i to, co je zadané v názvu. Ten je navíc o stránku výš, takže si toho člověk ani nevšimne. Můj první nápad bylo políčko prostě přejmenovat. Ve starých dobrých časech by to bylo triviální. Políčko bych změnil na <input name="place" value="<?php echo htmlspecialchars($row["name"]); ?>"> a kód, který ho zpracovává, na $row["name"] = $_POST["place"]. Přejmenovávat sloupec v databázi by bylo velmi složité, protože se v aplikaci používá na mnoha různých místech. Jak tohle udělat v aplikaci postavené nad Nette? Nepřišel jsem na to. Aplikace (nebo Nette?) automaticky provazuje formulářová políčka s daty v databázi a formulářová data zase posílá podle názvu do databáze. Asi by někam šlo doplnit tohle přejmenování, ale minimálně by to působilo jako hack.

Napadla mě jednodušší věc – políčku přidat atribut <input autocomplete>, což by v běžném formuláři byla práce na 10 sekund. Ve fóru jsem našel, jak se to dělá pro celý formulář s tím, že „pro jednotlivé inputy to není problém“. Pro mě to problém je. V dokumentaci jsem nic o nastavování vlastních atributů elementům nenašel. V API dokumentaci jsem našel metodu getControl, která vrací HTML element pro políčko. To mi přišlo dost podobné jako getElementPrototype doporučované pro nastavení autocomplete celému formuláři, tak jsem ji zkusil použít. Bohužel to ale nezabralo kvůli tomu, že metoda generuje kód pokaždé znovu a změny v dříve vygenerovaném HTML přijdou vniveč. Naštěstí jsem našel metodu setAttribute (později přejmenovanou na setHtmlAttribute), která vedla ke kýženému cíli. Povedlo by se mi to i pomocí metody getControlPrototype, která ale bohužel není v dokumentaci TextBase, kde jsem našel getControl. Je to proto, že pochází z rodičovské třídy a TextBase ji na rozdíl od getControl nepřepisuje. Navíc rozdíl mezi těmito metodami není nijak zevrubně popsán – čekal bych odkaz z jedné na druhou s vysvětlením rozdílu.

Proměnná na devu

Na vývojovém serveru jsem si chtěl nastavit nějakou proměnnou a použít ji v šabloně. Ve starých dobrých časech bych do config_local.php přidal define("MEDIA_ROOT", "https://...") a kdekoliv v kódu ji potom prostě vypsal. Jak to udělat v Nette? V config.local.neon si můžu vlastní proměnnou přidat do sekce parameters, jak ji ale pak použít v šabloně? Nepřišel jsem na to, tak jsem zvolil velmi krkolomné řešení poslání parametru do App\Model\Configuration (který se injectuje do presenterů), tam jeho uložení a následné ruční předání z prezenteru do šablony. Tuším, že tohle je úplně špatné řešení a že existuje mnohem jednodušší, ale v dokumentaci se o sekci parameters vůbec nepíše, natož aby tam bylo vysvětlené, jak ty parametry použít v jiných částech aplikace.

Závěr

Tímhle článkem rozhodně nechci říct, že abstrakce jsou špatné nebo že použité části (aplikace samotná, Nette, Dibi) nejsou dobré. Chci říct to, že každá abstrakce může něco zjednodušit, ale nejdřív se ji člověk musí naučit. Je jistě velmi pohodlné nestarat se o vytváření všech objektů použitých v aplikaci, ale pokud to nějaká abstrakce dělá za mě, tak je najednou mnohem pracnější to přizpůsobit. Stejně tak je pohodlné nemuset každé formulářové políčko vytvářet ručně, ale když ho potřebuji změnit, tak je to s abstrakcí opět složitější. Nebo vlastně ani ne, ale nejdřív tu abstrakci musím podrobně znát.

Dokumentace je v takovém případě klíčová. V uživatelské dokumentaci Dibi a Nette jsem třikrát nenašel to, co jsem potřeboval (onConnect, setAttribute, parameters). V API dokumentaci dvakrát ano, ale jednou v nepřehledné formě (onConnect) a jednou jsem sešel na scestí (getControl místo getControlPrototype). Jednu věc jsem nenašel vůbec (správné použití parameters), ale možná jen nevím, kde hledat. Na to, abych dokumentaci sám vylepšil, se necítím dost erudovaný – např. BaseControl má ještě nedokumentovanou metodu getControlPart, jaký je její smysl?

Kolem Nette je velmi aktivní komunita, takže když bych se zeptal, asi bych se dobral často k lepšímu řešení, než jaké jsem našel já. Ale už jen dobře zformulovat dotaz je těžká práce a čekat na odpověď se mi nikdy nechce – mám nějaký problém a chci ho vyřešit hned. Radši budu hodinu hledat v dokumentaci (a u toho třeba dozvím i něco dalšího) než deset minut formulovat dotaz a pak hodinu nebo den čekat na odpověď.

Jakub Vrána, Řešení problému, 9.12.2019, diskuse: 16 (nové: 0)

Diskuse

Kit:

Díky za skvělý článek, který odpovídá mým pocitům, které mívám u projektů v Nette. Pro své potřeby jsem si napsal poměrně krátkou třídu, kterou používám takto:
    $db = new MyPDO($config['MySQL']);
    $model = new Model($db);
... a mám DIC připraven. Stačí ho jen vložit do parametrů nějakého konstruktoru.

lenoch:

Díky, jsem rád, že existuje ještě někdo, kdo má podobné pocity. Není to problém Nette, které je na tom podle mě ještě dobře, ale většiny frameworků a nejen v php.

Často jsem strávil spoustu času přicházením na to, jak něco "ohnout" a "podsunout" abych donutil framework, který se urputně bránil, dělat to, co potřebuju.

Ale říkal jsem si, že možná nejsem dost intelektuálně na výši, že framework ví co dělá, když v něm něco nejde - atd.

Tím nechci říct, že frameworky jsou špatné - když děláte něco většího, tak buď nějaký použijete, nebo náhle zjistíte, že si programujete vlastní...

Svaťa:

Mám za to, že problém je filozofie Nette - hodně se toho dělá ''samo''. A když nemáte nastudováno/odprogramováno, jak toto ''samo sa to'' funguje, tak narážíte.

Další komplikací je konvence nad konfigurací. Když konvenci neznáte, nemáte se kde dočíst, jak je co nakonfigurováno... Teda krom zdrojáků.

Proto mám raději nástroje, které nejsou tak chytré, a dělají toho málo.

Taco:

Nebude to tím, že prostě neznáš tu filozofii? Když třeba porovnám Symfony, Nette, a Adminer - Symfony mi přijde takové už moc akademické. Nette je tak akorát, všechno mi jde na ruku, když něco nechci, nepoužiju a nepřekáží. Adminer jsem v rámci jednoho projektu chtěl vykuchat, a vzdal jsem to.

Není překvapující, že když neznáš styl a nebo dokonce máš jinej, tak to prostě drhne. IMHO ty máš hodně jinej styl než mnoho.

ikona Jakub Vrána OpenID:

Tím to podle mě není. Když chci něco udělat, tak se to v té abstrakci nejdřív musím naučit. Je celkem jedno, jaký ta abstrakce má styl, naučit se to musím tak jako tak.

David Grudl:

když technologie ujde dlouhý vývoj v tom, jak vytvořit lepší abstrakci, a následně dlouhý vývoj v tom, jak výslednou abstrakci maximálně zjednodušit, tak se stává pro člověka, který tyto dva body přeskočil, hodně nesrozumitelnou.

Zásadní koncepce, na které stojí dnešní vývoj, spočívá v tom, že žádný objekt nebo funkce nesahá na data mimo sebe. Protože jinak vzniknou skryté vazby, které nelze zpětně dohledat a které udělají z vývoje v určité fázi peklo. Takovými daty mohou být parametry, spojení s databází, ale vlastně i konstanty. Nesahat na tyto data znamená obrovský kotrmelec v tom, jak navrhovat aplikace. Pak přijde framework, který se snaží ruční předávání co nejvíce zjednodušit a automatizovat, a člověk který tyhle abstrakce nezná, je prostě ztracený.

Dá se to samozřejmě obcházet nebo kombinovat se „starým dobrým“ přístupem, ale lepší je fakt do té abstrakce proniknout.

Jen bodově k tomu na co jsi narazil:
- chtěl jsi po připojení k databázi provést nějaký SQL příkaz: na to je onConnect asi ideál, nicméně jsem netušil, že tam něco takového je, takže sám bych buď přidal setup do služby https://doc.nette.org/cs/3.0/configuring#toc-low-level-upravy nebo si vyrobil továrničku

- na přidání HTML atributů slouží metoda setHtmlAttribute(). Je to v dokumentaci věnující se vykreslování, což možná nemusí být zřejmé https://doc.nette.org/cs/3.0/form-rendering#toc-html-atributy. Ty ostatní metody jsou spíš interní.

- změnit name u políčka: to člověče nikdo za 10 let nepotřeboval :-) Ale že jsi to ty, poslal jsem commit a půjde to udělat přes setHtmlAttribute('name', '...')

- sekce parameters (v dokumentaci viz https://doc.nette.org/cs/3.0/di-configuration) funguje jednosměrně, tj. parametry můžeš posílat do jednotlivých služeb, ale nikdo jiný si na ně sáhnout nemůže. Viz koncepce výše. Takže myslím, že řešení přes App\Model\Configuration zní dobře.

ikona Jakub Vrána OpenID:

- V tom https://doc.nette.org/cs/3.0/configuring#toc-low-level-upravy, co je @resource? Jak to mám zjistit?

- Interní metody by měly být jako interní označené a mohlo by v jejich dokumentaci být uvedeno, co použít místo nich.

- Všiml jsem si anotace @inject a hledal jsem něco podobného pro pojmenované parametry (obdoba https://github.com/google/guice/wiki/BindingAnnotations#named). To jsem ale nenašel, přitom se mi to zdá šikovné.

David Grudl:

Co jsou to pojmenované parametry? Co je to za jméno v tom @Named?

ikona Jakub Vrána OpenID:

Cokoliv si tam vymyslíš. Na jednom místě svážeš String anotovaný třeba @Named("MediaRoot") s nějakou hodnotou a na druhém místě to pak injectuješ.

Gravid Doodle:

První Davidova věta naznačuje (implicitně, mezi řádky): „ty sis tím (vývojem, abstrakcí, zjednodušováním frameworku - a z kontextu: tohoto frameworku) neprošel, přeskočil jsi to, a tak samozřejmě je to tvoje vina, že je to pro tebe nesrozumitelné.“ Tento výrok se dá zobecnit nejen na Jakuba, ale na všechny programátory, kteří daný framework nevytvořili, jen ho chtějí použít. Samozřejmě, že pro ně bude nesrozumitelný. Jakpak by vůbec něco mohla být vina Nette nebo Davida Grudla? Radši svést vinu na jiné – to je ono. A ještě jeden závěr, který mě z této argumentace napadá, je ten, že v této logice má tedy vlastně smysl pracovat jen ve frameworku, který si člověk vyvine sám - to, že to v důsledku anuluje smysl psaní tohoto typu softwaru pro druhé, to trochu David nedomyslel.

>Dá se to samozřejmě obcházet nebo kombinovat se „starým dobrým“ přístupem

Tato věta právě znegovala celý smysl frameworků, všech výše zmíněných technologií obecně, a Nette konkrétně. A o to víc s přihlédnutím k tomu, že některé změny „trvají hodinu místo 10 sekund“.

> ...ale lepší je fakt do té abstrakce proniknout.

Toto říká David Jakubovi v kontextu: „poslouchej, cucáku, vůbec tomu nerozumíš, já ti ukážu, jak se to dělá“. A to ve stavu, kdy je k danému frameworku velmi špatná dokumentace (obecně známá věc, nadává na to každý programátor, který s Nette kdy dělal). Vlastně se ta věta dá přefrázovat na: „lepší je fakt být telepat a předvídat, co tou kterou nedokonalostí chtěl básník kódu říci, když to navíc nezdokumentoval [a je to tvoje vina, že to nevíš].“
Současně je to tzv. strawman argument – typ logického klamu, kdy se diskutující snaží atakovat něco, co jeho řečnický protivník neřekl nebo netvrdí (strawmanem v tomto místě je to, že by Jakub nerozuměl tomu, jak navrhovat aplikace a jak fungují objekty a jejich „sahání na data“). A zbytek Davidova příspěvku, kdy to má Jakubovi „natřít“, obsahuje jedno „to jsem vůbec netušil“, jedno „tadyto je (ne jednoznačně nebo určitě, ale) *spíš* takové makové“ a jedno „to dlouho nikdo nepotřeboval“ (což, asi uznáte, je argument na hov..).

Úžasné, Davide, krásně ses tu ukázal v pravém světle.

ikona Jakub Vrána OpenID:

Přiznám se, že já v tom tyhle věci nevidím.

Woytam:

Místo <?php define("MEDIA_ROOT", "https://...") ?> je možné použít sekci constants v config.local.neon
<?php
constants
:
   
MEDIA_ROOT: https://...
?>

ikona Jakub Vrána OpenID:

Jak to pak použiju? Je to někde dokumentované?

https://api.nette.org/3.0/Nette/Application/…_getContext je deprecated. Proč? Co se má použít místo toho? Proč to není popsané v dokumentaci?

Jan Barášek:

Ahoj Jakube,

docela dost mě mrzí tvůj pohled na abstrakci a že pořád zastáváš "starý dobrý" přístup. Osobně jsem zažil oba přístupy a k tomu starému bych se nikdy nevrátil, protože když má člověk ve správě stovky webů a potřebuje sdílet stejný kód v balících v rámci stovek projektů současně, tak je potřeba odebrat každou globální závislost a všechno navrhovat deklarativně bez magie.

Nette je za mě nejlepší framework na světě, protože jako jediný toto splňuje dokonale. Nutí vývojáře hodně přemýšlet. Nejprve rozdělit kód na služby, ty nakonfigurovat (a pečlivě zvalidovat konfiguraci) a až je teprve všechno správně, tak sám framework garantuje, že každá služba půjde kdekoli získat (pokud nenastane runtime chyba). Řeší to všechny praktické bolesti, které "staré dobré" řešení nikdy řešit nemůže. Když se například změní rozhraní nějaké vnitřní služby, tak na to Nette zareaguje přegenerováním DIC a tvůj kód to vůbec nemusí tušit a všechno bude fungovat dál.

Když nastane chyba, Tracy hezky ukáže cestu a všechny předané vstupy. Protože jsou data pečlivě oddělena a předávají se, tak lze dokonale vypátrat, kde jaká hodnota vznikla a proč. Nejsou tam tedy všechny nevýhody globálních proměnných a konstant, které mohou mít hodnotu jakoukoli a definovat se mohu kdekoli nebo musí existovat.

Ona striktnost k návrhu aplikací je možná z tvého pohledu vnímána negativně, ale z pozice vedoucího vývojového týmu vidím, jak obrovské výhody to přináší. Když nastane problém, jsem schopen pouhým pohledem na text výjimky a callstack ihned zjistit, co se stalo a hned máme best practice, jak to vyřešit. Starým "prasáckým" způsobem bylo něco takového nemyslitelné, protože existovaly desítky způsobů, jak vyřešit jeden a ten samý problém a každý programátor použil svoje oblíbené řešení a kód byl díky tomu dost magic a nekonzistentní.

Jakube, vím, že jsi skvělý programátor. Četl jsem celý tvůj web a celou tvoji knihu slovo od slova, tak se upřímně hodně divím, že v abstrakci nevidíš jenom výhody. Však jsem od tebe v Nette viděl spoustu commitů. ;)

A co říkáš na Doctrine? V kombinaci s Nette to je opravdová bomba.

ikona Jakub Vrána OpenID:

Proti abstrakcím v zásadě nic nemám, ale musí být jasné, jak je použít. Přesně vím, co chci udělat a jak se to dělá (a to mám ještě proti obvyklé situaci výhodu). Ale pokud mě dokumentace zavádí na scestí, tak mi celá abstrakce přijde na škodu.

Josef Jebavý:

"Staré dobré časy " já myslel, že jsem narazil na článek, který je aspoň 5 let starý.

Upravovat ty aplikace ze starých dobrých časů  je stress!

Diskuse je zrušena z důvodu spamu.

avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.