Dnes bych se chtěl ve svém krátkém blogpostu podělit o programovací techniku, kterou nazývám „vláček“. Myslím, že to nebude nic neznámého, je to věc, která se dá uplatnit nejen C++, ale většinou u všech objektových jazyků, kde je volání prováděno zkrze zápis objekt.metoda(), jako třeba Java. V některých jazycích najdeme vláčky v základu (smalltalk – říká se tomu „message chain“).
Asi nejznámější vláček v C++ pozná každý, je to zápis do std::cout.
std::cout << "Součet " << a << " a " << b << " rovná se " << c << std::endl;
Vláček poznáme snadno, většinou je na začátku lokomotiva, díky níž je vláček vláčkem. Kdyby tam lokomotiva nebyla, překladač by nám vynadal. Pak následuje větší množství vagonků, které jsou pospojované stejným způsobem, a na konci někdy bývá speciální ukončující vagón, tedy něco, co vláček ukončuje a tím způsobí nějakou akci. Nemusí nutně vždycky, záleží na tom, zda chceme do vláčku zapojovat další vagonky. A nebo, pokud si objekt nepřeje vytvářet neuzavřené vláčky, spustí se akce na prvním středníku (zpravidla díky destruktoru u pomocného objektu vytvořeném lokomotivou, který tvoří spojení mezi vagony).
Vláčky lze psát i jinak, než s použitím přetížených operátorů. Můžeme použít normální funkce a zápis může být flexibilnější a čitelnější. Například v minulém blogpostu jsem psal o jedné třídě na spouštění procesů. Ukážu vám příklad jejího použití právě ve schopnosti tvořit vláčky.
Process proc("cmd.exe");
ProcessEnv env = Process::getEnv();
///.... zde celkem nevýznamný kód pracující s env.
integer retval = proc
.setEnv(&env)
.arg("/C")
.arg(fname)
.arg(localBase)
.workDir(localBase)
.stdOut(execOutput)
.stdErr(execOutput)
.exec();
Nenechte se zmást, že vláček je rozepsán na víc řádků. Opět je na začátku lokomotiva, zde proměnná proc, za lokomtivou jsou napojeny vagónky různých typů. Najdeme zde vagónek nesoucí environment, pak následují argumenty procesu, nastavení pracovní složky, přesměrování výstupu do souboru a tím pomyslným ukončujícím vozem je zde metoda exec(), která nakonec určuje, jak se s vláčkem bude zacházet. Když se podíváte na začátek, najdete tam, přiřazení do proměnné retval. I toto specifikuje metoda exec().
Ukážeme si ještě další vláčky, třeba vláček ve vláčku:
PFactory_t f = connection.createJSON(); PNode_t r = f->newArray();
r->add(f->newObject()
->add(f,"url",x.url)
->add(f,"title",x.title)
->add("lastVisit",f->newArray()
->add(f,x.lastVisit.wYear)
->add(f,x.lastVisit.wMonth)
->add(f,x.lastVisit.wDay)
->add(f,x.lastVisit.wHour)
->add(f,x.lastVisit.wMinute)
->add(f,x.lastVisit.wSecond)));
Výše uvedený příklad představuje pole objektů obsahující atributy url, title a lastVisit, přičemž poslední proměnná je pole. Výsledkem je objekt uložený v JSONu.
Vláčky se nám hodí tam, kde potřebujeme k objektům nastavovat mnoho atributů, případně atributy nelze jednoduše nastavit konstruktorem. Příkladem může být třeba funkce na otevření souboru: openFile(„foo.txt“).create().truncate().append().lockExclusive().chmod(0644);
Deklarovat vláčkový styl je celkem snadné. Stačí, aby každá metoda, která má vystupovat jako vagónek, vracela referenci na objekt, který měnila (jednoduše na this). Pouze poslední „vagón“ buď obsahuje void, nebo vrací něco jiného.
class Prcoess {
public:
Process &arg(const char *arg);
Process &arg(natural number, natural base=10);
Process &arg(integer number, natural base=10);
Process &arg(float number);
Process &arg(double number);
Process &stdOut(const SeqFileOutput &stream);
Process &stdErr(const SeqFileOutput &stream);
Process &stdIn(const SeqFileInput &stream);
Process &setEnv(const ProcessEnv *env);
Process &workDir(ConstStrWpath);
intexec();
...
...
};
Pakliže potřebujeme, aby se akce automaticky provedla na prvním středníku, musí lokomotiva vrátit namísto sebe sama pomocný objekt, který obsahuje deklarace vagonku. Vagonky si nadale přenášejí pouze referenci na pomocný objekt. Tento objekt má také destruktor, který ve svém těle vyvolá předem danou akci.
Vláčkový styl jsem použil i pro sestavování SQL dotazů:
MySQL::Query_tq(...); q("REPLACE stats (`datum`,`counter`,`product_ver`,`partnerId`,`value`) " "VALUES (CURDATE() - INTERVAL %4 DAY,%1,%2,%3,%5)") .arg(iter->first.code) .arg(iter->first.versionId) .arg(iter->first.partnerId) .arg(iter->first.dateModif+age) .arg(iter->second.value) .exec();
Tady máme trochu komplikovanou lokomotivu, protože obsahuje vlastní SQL dotaz. Funkce arg jsou patřičně přetížené, takže řetězce například escapují tak, aby se zabránilo SQL-Injecting.
PS: kdyby to někdo hledal, oficiálně se tomu říká Method Chaining
@1 jo může být. Já teď koukám na Wikipedii a mají tam dvě hesla. Jen mi přijde že Fluent Interface je víc konkrétní, zatímco Method Chaining se označuje vše co vede na zřetězené volání metod.
Konečně první věta ve Wikipedii u hesla Fluent Interface říká: "A fluent interface is normally implemented by using method chaining".
Nicméně vláček taky není úplně můj výmysl, wikipedie u Method Chaining píše alternativní název "train wreck"
dakujem za blog, rad by som vedel o tejto problematike viac, ako navrhnut triedy aby bol ten method chaining najvhodnejsi, aby sa jednotlive metody navzajom doplnali a dali sa pohodlne retazit, tazko sa vysvetluje co mam na mysli, ale napr. prispevok @3 je dokazom toho, ze ma z toho bolia oci. niekedy musim doslova kombinovat co mi ktora metoda kedy vrati aby som to mohol zretazit s dalsou ... napr. v priklade od autora nadomnou ak by som chcel spravit x textEdit hexstring tak si viem predstavit tu strastiplnu cestu ako tam natrieskat metody tak, aby som to "pretransformoval" na vystup, ktory chcem, obcas to je len hole prehrabavanie sa v metodach co mi ktora ponukne ...
Člověk by asi měl k tomuto sáhnout pokud jde o čitelnost kódu. Například v situaci, kdy C++ ještě nemá dobře řešené volitelné množství argumentů se dá tento problém obejít například pomocí řetězení nějaké metody, která argumenty dodává. Viz můj příklad s SQL. Nebo je to krásně vidět u spouštění procesu, kde se člověk jinak nazlobí s tvorbou command line a definováním vstupů a výstupů.
@3 To je přesně ono, že tady už nejde o Fluent Interface, jedná se o konverzní řetěz. Něco podobného jsem tuhle vyrobil s MD5 třídou
MD5_t().append(login).append(heslo).append(salt).finish().asHexString().c_str()
Nicméně zkuste to napsat jinak na jednu řádku.
MD5toHex(MD5(login + heslo + salt));
Mimochodem, vláček bude rychlejší, protože tam nebude zbytečná concatace řetězců.
Někdy totiž rozpisování jednoho logického celku do více příkazů zavádí nepřehlednost, to pak je lepší sáhnout po samostatné funkci. Pokud se podaří provést zápis něčeho komplikovanější jako jeden příkaz, ušetří to práci, a nijak to nesnižuje čitelnost. Příklad s MD5_t jasně zobrazuje, že chceme vzít login,heslo,salt a udělat z toho MD5 v Hexa.
Někdy použití vláčků může generovat efektivnější kód. Například u třídy Process v implementaci ve Windows přidávání argumentů jednoduše zapisuje argumenty do nějakého interního bufferu obsahující příkazovou řádku. V Linuxové verzi se tvoří pole řetězců. Pokud bych na rozhraní zadával argumenty polem, vyrobím sice efektivnější Linuxovou verzi, ale neefektivní Windows verzi. Pokud bych argumenty zadával řetězcem, ve Windows je to super, ale musel bych pro Linux verzi implementovat parser Windowsovské příkazové řádky.
@10 s těma přetíženýma operátorama tam začne být problém, jakmile se někde uplatní priorita vyšší, než má ten operátor. Pak se tam začne člověk motat se závorkami. Nebo pokud je třeba z operátorů přejít na method chain.
(MD5_t() + login + heslo + salt).finish().asHexString().c_str();
Ale jo, používám to třeba na tvorbu tuplů
foo(xxx,(tuple,a,b,c,d,e,f))
@9 Používám výjimky často a try catchů minimálně. Většina výjimek je stejně odchycena na základní úrovni, protože jsou to jednoduše nepředvídatelné chyby. A v kódu odchytávám jen ty předvídatelné.
V Javě se fluent interface používá při implementaci builder patternu. Co zde bohužel nezaznělo, je to, že hlavní důvod pro jeho použití je postupné vytváření komplexního objektu s velkým množstvím volitelných parametrů/atributů, který ale ve výsledku musí být immutable.
Takový objekt není možné objekt nejprve vyrobit a teprve pak postupně nasetovat (že je zápis builderem kratší je už jenom bonus). Konstruktorem nebo factory metodou by se ten objekt sice vyrobil najednou, ale musely by mít hroznou hromadu argumentů (a pravděpodobně být ještě několikanásobně přetížené), v čemž by se ani prase nevyznalo. Builder tohle krásně řeší.
Příkladem budiž StringBuilder, request a response builder v Jersey, mimo JDK buildery intenzivně využívají třeba chlapci z Guavy.
Order order = webResource
.path("Orders")
.accept(MediaType.APPLICATION_JSON)
.header(API_KEY_NAME, API_KEY)
.header(USER_TOKEN_NAME, userToken)
.get(new GenericType() {});
Nic proti tomuhle blogu, popsané je to pěkně a člověk by měl o vláčcích něco vědět. Ale přiznám se, že je to styl programování, který mi nesedí. Mám rád jednoduché příkazy typu přiřazení, prosté volání funkce metody (která nevrhá žádné vyjímky), jednoduchou aritmetickou operaci s přiřazením a podobně. Složitější věci raději rozepíšu do víc příkazů. Přijde mi to přehlednější, jednodušší, blbovzdornější. Když se někde něco pokazí (ať už při překladu nebo za běhu), tak bývá jasné, kde je chyba, dá se to snadněji prokrokovat, proložit ladícím kódem atd. I v té konstrukci objektu a vykonání
new A().setB(b).setC(c).setD(d).show();
upřednostňuji prostou syntaxi
A a = new A();
a.setB(b);
a.setC(c);
a.setD(d);
a.show();
U toho vláčku speciálně mi vadí i to, že api musí být už na něj navržené. Když si pak někdo prohlíží dokumentaci nebo interface a nečeká tam vláček, tak může být docela zmatený.
Ono v tom API je to celkem jednoduchá volba. Pokud mám příkazy, které nic nevrací, mám tam nechávat void nebo referenci na sama sebe? V zásadě to nic nestojí (pravda v nativu bez optimalizace je to většinou jeden mov z registru ECX do registru EAX).
Pokud tam nechám referenci na sebe sama, nebrání mi to psát to klasicky. Pokud tam dám jen void, nemohu to už nikdy více řetězit.
Muzete mi nekdo vysvetlit jak je do doopravdy(podle standartu) s zivotnosti docasne vytvorenych objektu? Mam takovyhle priklad:
std::cout << someQTString.toStdString().c_str() << someOtherQTString.toStdString().c_str();
Pokud pouziju "vlacek" s presmerovanim, tak mi to pod MSVC spadne protoze docasna instance std::string je zrusena a c_str() vraci pointer na neexistujici objekt.
Pokud pouziju vlacek .arg(a).arg(b).arg(). Tak to funguje.
@17 Pokud je to reference, dočasná proměnná existuje do konce výrazu (do středníku), pokud je to přesun (nové v C++11 a nepoužívá se u tohoto operátoru), dočasná proměnná existuje do konce funkce, kam je předána (do uzavírací závorky). Ani v jednom případě by to nemělo spadnout.
Btw. pokud .arg(someQTString.toStdString().c_str()) funguje, jak by dopadlo .operator <<(someQTString.toStdString().c_str()), které je ekvivalentní použití operátoru <<?
V Jave su o tom aj cele knihy. Tu je nejaky zakladny popis aj s kompletnym popisom techniky a nejakymi uvahami naokolo. Treba hladat "DSL in Java"
http://weblogs.java.net/blog/carcassi/archive/2010/02/04/building-fluent-api-internal-dsl-java
http://www.javaworld.com/javaworld/jw-06-2008/jw-06-dsls-in-java-1.html
Dalsie resource - odporucam Martin Fowler-a:
http://tech.puredanger.com/dsls-in-java/
Nasit to na C++ asi nebude velky problem.
std::cout << "Součet " << a << " a " << b << " rovná se " << c << std::endl;
tak presne diky tomuhle zapisu jsem poznal, ze C++ je kram (alespon pro me) uz kdyz sem videl prvni hello world. a kdyz sem zkoumal dal, tak se to potvrdilo tim, ze stejny kod byl po zkompilovani v gcc rychlejsi nez v g++
[23] To není vtip, tak se skutečně ve WinAPI programovalo. Ten "boilerplate" byl příšerný, rádobyobjektové programování v C bylo utrpení, maďarská notace byl taky "skvělý" vynález a člověk si pořád musel dávat pozor, co musí dělat napřed a co potom a když to nefungovalo, slabší povahy smazaly celý kód a psaly znovu. Pak přišlo IDE od Borlandu, které umělo detekovat podobné problémy a jakž takž to šlo. Myslím, že i Microsoftu velice záhy došlo, jaké monstrum zplodil a přišel s MFC, které už se dalo používat daleko snáze.
[26] Jasně, to nepopírám. Myslel jsem to tak, že v dřevních dobách se to psalo takhle přímo normálně; samozřejmě, že i dnes lze jet přímo ve WinAPI, nehledě na to, že funkcím WinAPI se člověk obyčejně nevyhnul úplně ani při použití MFC. Proti vlastním třídám nic, pokud mají opodstatnění.
Intenzivně se zabývám programováním zejména v jazyce C++. Vyvíjím vlastní knihovny, vzory, techniky, používám šablony, to vše proto, aby se mi usnadnil život při návrhu aplikací. Pracoval jsem jako programátor ve společnosti Seznam.cz. Nyní jsem se usadil v jednom startupu, kde vyvíjím serverové komponenty a informační systémy v C++
Přečteno 57 600×
Přečteno 27 720×
Přečteno 26 403×
Přečteno 24 367×
Přečteno 22 864×