[ Webhosting ProFiTux.cz ] [ Magnetické neocube ] [ Sex po telefonu 100% živě ] [ barevne holinky ] [ mp3 zdarma ] [ mp3 ke stažení ] [ Články o vaření ] [ SPORTOVNÍ VÝŽIVA ] [ Sun-shop ] [ Světové problémy ]
[ Pro ženy ] [ mp3 ke stažení ] [ Kámasútra video ] [ První sex ] [ ILike.Cz HQ tapety na plochu ] [ www.Elektro-Cigareta.cz ] [ Zlevněte si pojištění! ] [ Školení tvorby webu ] [ Zvýšení návštěvnosti ]

jakubův notes – programování a vejšplechty

Píšu o:

Frontend-backend problém

Nikdy nepodceňujte to, co se vám urodí v hlavě, když ležíte ve vaně! Nevím, jestli to bylo pěnou vlétnuvší do nosu, nebo uvolněním celého těla včetně mysli zapříčiněným horkou vodou, avšak hlavou mi projela otázka, co je lepší/jednodušší/efektivnější způsob vývoje – frontend první, nebo backend první?

U webových aplikací je to povětšinou tak, že existuje frontend, který je zobrazován „běžným smrtelníkům“, řekněme uživatelům, kterýmžto je poskytována služba (ať se jedná o možnost něco koupit, prodat, přečíst si), kterou někteří i využijí; a pak backend, k němuž mají přístup jen „ti nejpovolanější“, administrátoři, kteří se starají o to, aby měli uživatelé co k užití.

Opět ve většině případů se jedná o více, či méně oddělené části. Je vůbec záhodno je vyvíjet odděleně? Podle mě ano – každá slouží pro někoho jiného, a proto v uživatelském rozhraní je kladen důraz na něco jiného. Na frontendu se může grafik řádně vyřádit a kvůli přístupnosti musí být kladem důraz na to, aby šel používat i bez různých AJAXovátek. Taky vidím potřebu být více výřečný – ne každý potencionální uživatel je počítačový expert. U backendu nebude střídmější grafika na škodu – hlavní je umožnit administrátorovi udělat co nejrychleji, co je potřeba; proto AJAXovátka jsou vítána (od administrátora/-ů se už může vyžadovat, aby jeho/jejich prohlížeč splňoval daná kritéria).

Frontend i backend tedy vyvíjejme odděleně. Co bude lepší, začít s frontendem, nebo s backendem?

Řekněme, že první přijde na řadu frontend. Vžijeme se do uživatele webové aplikace (ne, metodě, co by chtěl jakýsi záhadný „běžný uživatel“, nevěřím) a začneme vyvíjet. Datový model může doznat velkých změn podle toho, jak zkoušíme, co vyhovuje uživateli víc (může se do toho přidat i uživatelské testování), a pořád je tak potřeba generovat nová a nová „ukázková“ data (což může být při nějakých složitějších strukturách nesnadné /např. při použití traverzování kolem stromu/). Při začatí backendem je to naopak – máme jednoduše získaná ukázková data (o všechnu složitou manipulaci se stará daný kód), ale nemáme, kde bychom je ukazovali a nakonec zjistíme, že tahle data se ani použít nedají (přichází tak na řadu refaktoring kódu backendu).

U začatí backendem nevím, co je větší problém – že se může změnit datový model při dělání frontendu, nebo že není, co prezentovat zákazníkovi. Resp. on je k prezentování ten backend. Ale jen těžko odpovědět na otázku: „Dobře, ale já chtěl e-shop; jak se v tom tedy nakupuje?“ Ovšem podobná otázka může nastat i při přístupu z opačného konce („A jak můžu spravovat zadané objednávky?“). A ne, odpovědí není ještě více omezit funkčnost prototypu – to už nejde; stejně jako je to s bičíkem bakterií (a máš to evoluce!), je nějaký jistý základ, který pro to, aby to vůbec fungovalo, potřebujete. A navíc, o to se nejedná; i kdyby to šlo více osekat, stále je tu otázka, jestli začít front-/backendem.

Mimochodem, chtěl jsem tohle nazvat jako problém čtenáře a zapisovatele (reader writer problém), ale tohle už někdo ukradl dříve. Uživatel je totiž ten, kterému je z převážné většiny něco prezentováno, a administrátor je ten, kdo zapisuje. I když se pozice „převážně čtenáře“ se u dnešních sociálních aplikací posouvá k vyvážení čtení a zapisování (možná i převaze zapisování) z pohledu uživatele; administrátor má spíše pozici korektora/usměrňovače/mazače (i tak /právě proto/ ale budou na administraci kladeny jiné nároky než na frontend).

vydáno 5. 11. 2009, 21:58:01

žádný komentář

Zařazeno mezi:

shower – prezentace s videy

Hledal jsem nějakou elegantní možnost, jak udělat prezentace s vloženými videy. OpenOffice.org Impress cosi pořád kecal o kodecích a nechtělo se mi to řešit. Pravděpodobně by to nějak šlo s LaTeX Beamerem vložením videa do PDF. Beamer může být skvělá věc, ale nechtělo se mi laborovat s tím, jak tam vytvořit vlastní téma, protože žádné z výchozích se mi pro danou prezentaci nezdá.

Nakonec jsem skončil u osvědčené (pro většinu lidí asi trochu brute-force a ani ne moc elegantní) kombinace Qt + mplayer a naprogramování si prohlížedla přesně na míru. Qt spolu s mplayerem jsem už jednou úspěsně použil; pro dvb-starter. A protože jsem narazil na způsob, jak mplayer vložit přímo do Qt widgetu (shodou okolností nemůžu to fórum, kde jsem to našel, znovu objevit), nic nestálo v cestě se na to vrhnout.

Embedding (krásné to slovo) mplayeru v Xkách je velice jednoduché – stačí mu přes volbu -wid následovanou číslem Xkového okna (v Xkách je oknem prakticky všechno; ve většině toolkitů je snad každý widget reprezentovám Xkovým oknem) předat, kam má video vykreslovat. Problém byl v tom, že mplayer video roztáhne na celé okno – a tedy je povětšinou deformované. Naprostá souhra náhod tomu dala a já otevřel dnes jeden post na jednom blogu na Abclinuxu a tam přesně to, co jsem potřeboval – přidání černých pruhů kolem videa pomocí filtru (-vf) expand. Věc je pak úplně jednoduchá (spustí přehrávání video.avi v daném widgetu):

QProcess *mplayer = new QProcess(widget);

mplayer->start(
    "mplayer", QStringList() <<
    "-wid" << QString::number(widget->winId()) <<
    "-slave" <<
    "-vf" << QString("expand=:::::%1").arg((double) widget->width() / widget->height()) <<
    "video.avi"
);

Metoda winId() vrací číslo Xkového okna, které se převede na řezězec a předá spouštěnému mplayeru, čímž řekneme, že má video vykreslovat do daného widgetu. Video filter expand bere několik parametrů (jako je pozice x, y; šířka, výška apod.). Důležitý je ale poslední – desetinné číslo specifikující poměr stran. Ostatní parametry se doplní, aby to vycházelo co nejlépe (největší možná velikost, vycentrované umístění…).

Jak tohle člověk má, stačilo přidat vykreslování obrázkových slajdů a shower byl na světě. Jak jinak než, že kód je na GitHubu:

$ git clone git://github.com/jakubkulhan/shower.git

Doufám v co nejmenší počet SEGFAULTů při prezentaci :-)

vydáno 20. 9. 2009, 16:27:00

žádný komentář

Zařazeno mezi:

B+Tree v PHP

Když jsem konečně rozchodil metatable, byl to prakticky tantrický okamžik. Och, ono to vážně funguje! A ukládá! A vybírá! Jako když malé děcko dostane lízátko. Navíc, já jsem si to lízátko sám i udělal.

Ale neusnul jsem takříkajíc na vavřínech a šel jsem ještě dál. Když si vezmete snad jakoukoli databázi, tak najdete B+Tree, další B+Tree a ještě více B+Tree. Inu, proč se neinspirovat a neudělat taky takový B+Tree? Samozřejmě zcela v PHP.

A tak vznikla třída btree, která umí spravovat B+Tree uložený v souboru. API je opět minimalistické a bude vám stačit osvojit si jen pár metod – v případě btree si vystačíte s polovinou metod než u metatable (celkem tedy se třemi). Okolo metatable jsem udělal velký humbuk, u btree se spokojím s oznámením tady na blogu.

GitHub jsem si docela oblíbil, takže opět můžete zdrojový kód získat z repozitáře právě tam:

$ git clone git://github.com/jakubkulhan/btree.git

Nejdříve ze všeho je potřeba si vytvořit instanci. Zase je tu statická metoda open():

$btree = btree::open('my.tree');

Klasicky (alias stejně jako u metatable), pokud vše proběhlo v pořádku, open() vrátí novou instanci btree, jinak FALSE. Proč všude cpeš „oupn“ metodu? Samozřejmě by šlo nechat konstruktor jako public a v případě nějaké chyby vyhodit výjimku. Avšak nemohu si pomoci – výjimky prostě nemám rád. Objekty jsou naprosto skvělé, dokonce i do Céčka (takový ten dřevní jazyk; jen pro fajnšmekry) přenáším některé své návyky z OOP. Ale výjimky? Dílo ďáblovo! Hlavně, když dělají něco, co by člověk nečekal.

Když už jeden má instanci btree, bylo by záhodno si do stromu něco uložit:

$btree->set('key', 'value');

(Opravdu jsem nepřišel na chytřejší název metody.) Narozdíl od metatable může jít jako druhý argument, jako hodnota, cokoli, co se dá serializovat, a ne jen určité typy. Ale stejně jako je tomu u metatable, vymazání určitého klíče se provádí přiřazením NULL:

$btree->set('key', NULL);

Pro získání je jako u metatable metoda get():

assert($btree->get('key') === 'value');

Tohle je všechno, co budete potřebovat, budete-li chtít použít B+Tree. Minimalistická API, to je moje.

U metatable jste se museli starat o uzavírání – buď předáním flagu AUTOCLOSE při otevírání nebo ručně. U btree tomu tak není, protože veškeré úpravy stromu probíhají append-only – tzn. že se nikdy nepřepisují již zapsaná data. Má to hodně výhod. Nemusíte se bát, že by se data nějak poškodila. Prakticky nejsou potřeba zámky (bohužel jsem nevymyslel, jak udělat set() bez zamykání; ale zatímco jedna instance zapisuje, můžete stále v klidu číst pomocí get()). A také to znamenalo velké zjednodušení kódu oproti metatable, kde k zajištění konzistence dat se všechno muselo kopírovat do *~handle souboru.

Co se tedy stane, pokud např. v prostředku zápisu nějakých dat btree vypadne elektřina? Nepůjde to vysvětlit bez toho, abych popsal formát souboru.

Jelikož strom se skládá z jednotlivých uzlů, tak i soubor se musí skládat z uzlů. Navíc se uzly do souboru musí ukládat „ploše“, jelikož soubor je jako jeden dlouhý řetězec jedniček a nul. Každý uzel v souboru je reprezentován jako 32-bitové bezznaménkové číslo (formát N pro pack()) následované počtem bytů uložených právě v tom čísle. První dva byty určují typ uzlu a pak je serializovaná podoba uzlu.

Jsou pouze dva typy uzlů – kp a kv. První typ – kp (key-pointer) – je asociativní pole obsahující dvojice (klíč, ukazatel), kde ukazatel je integer ukazující na jiný uzel (offset uzlu v souboru). Druhý – kv (key-value) – je vždy listem stromu a obsahuje již dvojice (klíč, hodnota). Nebudu nic zapírat, inspirací pro mě byla CouchDB.

CouchDB B+Tree

CouchDB B+Tree (reduce hodnot si nevšímejte, tohle má CouchDB kvůli pohledům a btree žádné pohledy nemá)

„Vždy“ na konci souboru je ukazatel na kořen stromu následovaný „hlavičkou“ sestávající z řetězce "\xffbtree" (\xff je byte plný samých jedniček).

Řekněme tedy, že ukládáme nějakou hodnotu. Jak se tomu u B+Tree dělá, traverzujeme stromem až do listu (kv uzlu), kam by hodnota měla být uložena. Udělá se vše potřebné (rozdělování/slučování uzlů) a postupně se všechny ovlivněné uzly zapisují do souboru – připojují na konec. Na závěr je připojena nová poloha kořene s novou hlavičkou a je vynucen zápis na disk. Když se kdykoli od začátku zápisu do doby, než je všechno úspěšně na disku, něco stane, btree si z toho nic nedělá: Je zapsána třeba jen polovina uzlů? Chybí hlavička? To mě nerozhází!

Při hledání kořene se totiž ověřuje, jestli je na konci souboru opravdu celá hlavička, a pokud ne, btree se od konce souboru dobírá k poslední úspěšně zapsané hlavičce. (Pozor tedy – uložením řetězce "\xffbtree" můžete btree pěkně pomotat hlavu!) To je celé kouzlo. Žádné opravovací algoritmy, nebo žurnály nejsou potřeba, protože i když je soubor „rozbitý“ nedokončeným zápisem, strom je v posledním konzistentním stavu. Všechna data, na která není odkazováno, jsou považována za „neexistující“.

vydáno 21. 8. 2009, 23:15:13

žádný komentář

Zařazeno mezi:

Zkušenosti se Zend_Search_Lucene

Jak se praví v dokumentaci, Zend_Search_Lucene je obecný textový vyhledávací engine napsaný pouze v PHP5. Praktické využití pro většinu webů – fulltextové vyhledávání. Osobně jsem si zvykl spíš používat Google a jeho site:domain.tld. Ale mít vlastní fulltextové vyhledávání je lepší, jelikož Google mě nemusí zas tak rychle zaindexovat a uživatelé jsou na zvyklí.

Zend je celkem moloch, takže nedoporučuji stahovat celý archiv, ale namísto toho využít jen SVN repozitář a provést checkout pouze potřebných adresářů a souborů:

$ mkdir -p lib/Zend
$ svn checkout \
http://framework.zend.com/svn/framework/standard/trunk/library/Zend/Search \
lib/Zend/Search
$ svn cat \
http://framework.zend.com/svn/framework/standard/trunk/library/Zend/Exception.php \
> lib/Zend/Exception.php
$ find lib/ -name ".svn" | xargs rm -rf

Zend potřebuje, aby byla správně nastavena include_path. Je-li framework umístěn v podadresáři lib/, pak je potřeba include_path nastavit nějak takto:

set_include_path(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'lib' .
    PATH_SEPARATOR . get_include_path());

Ještě než se člověk pustí do práce s indexem, je potřeba nastavit locale. Tohle mě docela potrápilo. Na localhostu všechno fungovalo, jak by mělo, ale na produkčním serveru se výsledky hledání nějak podivně omezily jen na pár věcí – jako by se nezaindexovalo posledních pár slov. Zkusil jsem vše přeindexovat asi 3krát, ale pořád to samé. Kdyby mě rovnou napadlo se podívat do chybového logu, mohl jsem serveru ušetřit hodně procesorového času. Nejdříve jsem si logu vůbec nevšímal, ale jak dosáhl velikosti někde kolem 3 MiB, přilákal tím tak trochu mou pozornost. Problém byl v jiném nastavení locale u mě doma a na produkčním serveru, takže iconv() skončilo s Notice o nepovoleném znaku, čímž se zpracovala jen část řetězce. Doma jsem měl nastaveno UTF-8, na produkčním je asi ISO-8859-2, takže setlocale():

setlocale('cs_CZ.UTF-8');

Fulltext jsem potřeboval pro jednu instanci Shopaholic. Nakonec jsem se rozhodl, že indexovat se budou akorát produkty – u e-shopu lidi stejně nic jiného nezajímá (alespoň doufám). (Jelikož jsem na této instanci provedl ještě nějaké úpravy, které by nebylo dobré zahrnout do hlavní větve, bude chvilku trvat, než se fulltext dostane do repozitáře na GitHubu.)

Celé indexování se Zend_Search_Lucene probíhá ve vytváření dokumentů a jejich ukládání do indexu. Každý dokument obsahuje různá pole, pomocí kterých se dá vyhledávat. Jedno pole je pro vyhledávání výchozí, a tak se při psaní dotazu nemusí uvádět jeho jméno.

Pole jsou různých typů a mají různé vlastnosti. Pro aplikace, kde slouží jako persistentní úložiště databáze, je podle mě nejlepší zvolit pro všechna pole typ UnStored – bylo by zbytečné data z databáze ještě replikovat v indexu. Jediné, co Shopaholic ukládá do indexu, je ID produktu, podle kterého se poté při vyhledávání podnikne dotaz na databázi a vyberou skutečná data.

Když se na to podíváme z toho pohledu, že data jsou v databázi, pak možnosti indexování jednotlivých polí ztrácí význam. Řekněme, že název produktu je ukládán do pole name výrobce do manufacturer a popis produktu do description. Jako výchozí se zvolí pole description. Ale teď chce zákazník vyhledat všechny produkty od výrobce foobar. Aby tak mohl učinit, musel by do vyhledávacího políčka zadat manufacturer:foobar. Ale kdo tohle udělá? Zákazník chce prostě zadat foobar a mít výrobky od společnosti foobar.

Takže všechny informace, které by mohl chtít zákazník hledat, je nakonec potřeba dát do jednoho pole a to nastavit jako výchozí. Zvažuji, jestli do hlavní větve ponechat i další pole (kvůli pokročilejším dotazům), nebo se jich úplně zbavit a nechat jen jedno jediné? Spíš zbavit, Shopaholic má směřovat k co největší jednoduchosti – easy yet powerful, a ne powerful yet easy (protože to stejně nikdy pak easy není).

Potom tu vyvstává problém, jak aktualizovat index, když se některý z produktů změní. Jsou dvě možnosti, každá má svá pro a proti:

  1. aktualizovat index hned při jakékoli změně produktu

  2. vyčlenit změny zvlášť

První možnost přidá, a to docela dost, na čase všech operací, takže práce s produkty je pomalejší. Ale zase zachovává index neustále aktuální a zákazník vždycky najde to, co je právě teď v databázi. U druhého řešení je to naopak – práce s produkty je plynulejší, ale zákazník nemusí vždy najít to, co vlastně chtěl, protože se to mezitím mohlo změnit. A taktéž přidává nutnost do rozhraní nějak tu volbu k aktualizaci fulltextu zapracovat. Vzhledem k importu nakonec vyhrála druhá možnost.

Jak ukládat informace o tom, že produkt změnil? Databázové schéma postrádá informace o tom, kdy byl produkt vložen, či upraven (jaké to špatné mé návrhové rozhodnutí!), takže tudy cesta nevede. I když teď mě napadá, že by se to dalo sledovat pomocí změn cen – při každém ukládání produktu je zároveň zapsána cena, s kterou je v ten okamžik ukládán. Proč mě to nenapadlo dříve?

Vyřešil jsem to tedy jinak – bitmapou (a teď myslím tu bitmapu, tu strukturu, ne obrázek) ukládanou do souboru. Kód na konci článku.

Zend_Search_Lucene mě potěšilo – jak svou jednoduchostí, tak rychlostí. Originál Lucene je asi rychlejší, ale na PHP to podle mě vůbec není špatné. Taky mě to utvrdilo v tom, že Zend má své dobré části – Zend_Search_Lucene, různá API –, ale celkově je to moloch a jako základ k psaní aplikací bych ho nepoužil.

function bitmap($filename)
{
    // get all true
    if (func_num_args() === 1) {
        if (($data = file_get_contents($filename)) === FALSE) return NULL;
        $bits = array();
        for ($i = 0, $len = strlen($data); $i < $len; ++$i) {
            $byte = ord($data{$i});
            for ($j = 0; $j < 8; $j++) 
                if ($byte & (1 << (7 - $j))) $bits[] = $i * 8 + $j;
        }

        return $bits;

    // get/set bit
    } else {
        $bit = intval(func_get_arg(1));
        $set = NULL;
        if (func_num_args() > 2) $set = func_get_arg(2);
            
        // open file
        if (!($handle = @fopen($filename, 'r+b'))) return NULL;
        if (!flock($handle, $set === NULL ? LOCK_SH : LOCK_EX)) {
            fclose($handle);
            return NULL;
        }

        // read byte with needed bit
        $offset = intval(floor($bit / 8));
        $byte_offset = 7 - ($bit % 8);
        if (fseek($handle, $offset, SEEK_SET) === -1) {
            fclose($handle); 
            return NULL;
        }
        $byte = ord(fread($handle, 1));

        // get
        if ($set === NULL) {
            fclose($handle);
            return (bool) ($byte & (1 << $byte_offset));
        }

        // set
        $byte = ($a = ($byte >> ($byte_offset + 1) << ($byte_offset + 1))) |
            $byte & ~($a + (1 << $byte_offset)) |
            intval((bool) $set) << $byte_offset;

        if (fseek($handle, $offset, SEEK_SET) === -1) {
            fclose($handle);
            return NULL;
        }

        if (fwrite($handle, chr($byte), 1) !== 1) {
            fclose($handle);
            return NULL;
        }

        fflush($handle);
        fclose($handle);
        return TRUE;
    }
}

vydáno 28. 7. 2009, 15:02:51

2 komentáře

Zařazeno mezi:

PHP curry

U funkcionálních jazyků je velice častý tzv. „currying“, neboli možnost vytvořit novou funkci dosazením nějakých výchozích argumentů do již existující funkce. Jak na to v PHP?

Příklad z Haskellu:

add a b = a + b
add3 = add 3

[ pokračovat ve čtení… ]

vydáno 20. 7. 2009, 10:00:00

žádný komentář

Zařazeno mezi: