ugrás a tartalomhoz

assert() - mesél a forráskód

gerzson · 2004. Nov. 30. (K), 07.30
assert() - mesél a forráskód
A cikksorozat első részében az elméleti alapok lefektetése után épphogy csak belekóstoltunk a gyakorlati felhasználás lehetőségeibe. A mostani részben már jóval több példát láthatunk arra, hogy első üdvöskénket, az assert() függvényt, miképpen foghatjuk munkára (vagy hadra?) hibamentesebb PHP programok írásáért, és az úgynevezett "élő dokumentáció" kialakításához.

A sorozatban megjelent

Röviden összefoglalva az első cikk gondolatait, azt mondhatjuk, hogy az assert() függvény használatával képessé válunk eddig a program tényleges szerkezetétől külön életet élő feltételeket, feltételezéseket a forráskód részévé tenni többé-kevésbé testreszabható módon. A kód részeként időről-időre, vagy éppenséggel állandó jelleggel teszteljük ezeket a feltételezéseket, mivel az ezeket körbevevő kóddal együtt ezek a tesztek is lefutnak; ez teszi őket "élővé".

A dokumentáció célja, hogy a formalizált megadás mellett (értsd forráskód) emberi fogyasztásra is alkalmas formában, deklaratív módon megfogalmazva is rendelkezésre álljon, hogy egy adott funkció milyen körülmények között, milyen bemeneti kombinációkra milyen eredményt ad. E kettőből alakul ki az "élő dokumentáció". Mindezt az összetett szerepet hogyan töltheti be egy olyan egyszerű függvény, mint az assert()? Töredékesen teszi ezt csak, de még így is elegendő erőt és lehetőséget adva a fejlesztők kezébe. Az alábbiakban két kisebb mintapéldaszerű aprócska függvénykönyvtár építése kapcsán leshetjük meg mindezt akció közben.

Itt a weblabor.hu oldalain biztos sokaknak szemet szúrt az az apró, de nagyszerű szolgáltatás (nem én írtam, nem haza beszélek!), hogy a folyószövegekben megjelenő hosszú URL-k automatikusan rövidítve jelennek meg, miközben linkként továbbra is elérhető a teljes URL. Először ezt vesszük szemügyre!

Fejlesztés alatt

Mit várunk egy ilyen URL megjelenítőtől? Mielőtt nagyon előreszaladnánk e cikk mintapéldáihoz, szedje össze mindenki, hogy számára mi a fontos egy ilyen funkcióban, vonatkozzon az a felhasználására, paraméterezhetőségére, bővíthetőségére vagy implementálására! Ezt a gondolat kísérletet érdemes elvégezni kisebb-nagyobb körben is, és észrevenni, ki mit tekint alapvetőnek, magától értetődőnek, triviálisnak, stb. És hogy ebből mennyit írt le a saját listájára!
Az én listám folyamatosan bővülni fog, de kezdetben induljunk ki ebből:
  • Egyszerű függvény-interfész, értsd ehhez nem kell objektum-orientált megközelítés!
  • Paraméterezhető legyen a megjelenítési hossz maximuma.
  • Paraméterezhető legyen a rövidítésre használt jelölés, ami tipikusan "...", de ki tudja?
  • Szabályozható legyen a rövidítés helye és módja, azaz lehessen tippeket adni az implementációnak, hogy hol szabad vagy ajánlott a rövidítést elvégeznie, s mely részeket hagyja érintetlenül.
  • Az algoritmus elég általános legyen ahhoz, hogy más típusú szöveges adatok rövidítésére is használható legyen.

Aztán adjunk is nevet neki mindjárt: legyen shrlink (shrink + link)!
<?php // 0.) a mintapélda
function shrlink($url)
{
    return "<a href='$url'>$url</a>";
}
?>
Ez nem vicc, csak éppen röhejes. Mint azt az előbb írtam alapvetően kényelmi szolgáltatásról van szó, az oldal enélkül is képes lenne megjelenni és működni. Ennek megfelelően a megvalósítása késedelmet is szenvedhet akár, sőt néha még hibázhat is mindaddig, amíg az eredeti információ, az URL maga, kinyerhető marad. Ez a 0. verziós megoldás a kezdeti 5 kitételünkből jóformán csak egyetlen egyet teljesít, de tovább dolgozhatunk rajta, ha ismét lesz kis szabadidőnk és nem felejtjük el addig! Hogy el ne felejtsük, bemutatom az első felhasználási módját az assert()-nek:
<?php // 1.) TODO assert()
function shrlink($url, $max)
{    // TODO: $max-nal hosszabb $url-ket vagni kell valahogyan!
    assert ('strlen ($url) <= $max');
    return "<a href='$url'>$url</a>";
}
?>
Feltűnhet, hogy a TODO assert csak akkor képes figyelmeztetni a fejlesztőt a hiányzó, kidolgozandó funkcionalitásra, ha olyan bemenetet kap ($url és $max), ami megsérti a felállított feltételt. Ezzel ellentétben a megjegyzés - a hagyományos dokumentáció része - roppant informatív, közérthető, és világosan mutatja a félkész állapotot. Sajnos ez mind igaz, de van néhány egyéb megfontolandó érv! (Ezért nincs itt vége a cikknek.)
  • A dokumentáció akkor éri el célját, ha azt valaki valaha el is olvassa. A kódba épített feltételnek ennél több esélye van sokszor, hiszen a programot tesztelés céljából általában futtatni szokás, és így szóhoz juthat az assert() is. Az olvasás analógiájánál maradva: a kódkönyvet legalább biztosan előveszik, csak az a kérdés, hogy az adott oldalt el is olvassák-e.
  • A két megközelítés nem zárja ki egymást, sőt segítheti a megértést bonyolultabb kifejezések esetén. Mint említettem az assert() nem megoldás mindenre, tehát ne dobjunk ki semmit, ami már bevált, de merjük használni kiegészítésként!
  • A feltételek félig kódba öntött megfogalmazása előrevetít(het)i a megoldás módját, és referenciaként szolgálhat a szóban megadott leíráshoz.
  • Bármilyen "igazi" feltétel egyszerűen örökösen hamissá változtatható, pl.: assert('$TODO && strlen($url) < $max');, amivel kikényszeríthető, hogy a shrlink() hívásakor mindig figyelmeztető üzenet generálódjon, és ne sikkadjon el a tennivalók tengerében.
  • A tesztadatokat úgy kell kiválasztani, hogy azok a lehető legtöbb aspektusát lefedjék a tesztelendő funkciónak. Nem kell sok turpisság ahhoz, hogy olyan bemenetet generáljunk a shrlink()-nek, hogy a benne szereplő feltétel ne teljesüljön.
  • Ám ha az adott funkció nélkül is hibátlanul működik a program, akkor nem pocsékoltunk időt "haszontalan" dolgokra. Ez utóbbi gondolat némi rokonságot mutat az "extreme programming" irányzattal, ahol az új funkciók implementálását mindig valamilyen "kívánság-feltétel" sérülése/meghiúsulása indítja be (coding on demand). Ha nincs ami meghiúsuljon, nem kell semmi újat kifejleszteni.

Az előzetes kívánságlistánk első két pontja már felfedezhető a shrlink() függvényünkben: sima függvény interfész és paraméterezhető maximum megjelenítési hossz. A rövidítéshez használt jelölés hozzáadása a $max-hoz hasonlóan történik, de a rövidítés módjának és helyének szabályozása már több gondolkodást igényel.

A felszín alatt - interfészhez közeli alkalmazások

Tekintve, hogy az URL struktúrált szöveg, célszerű lenne, ha a rövidítés helyét (vagy helyeit?) URL tagjaira hivatkozva lehetne megadni. Ezzel szemben viszont az algoritmusnak elég általánosnak kell lennie, hogy kiterjeszthető legyen más formákra, típusokra is. (Figyelem, most éppen tervezési döntést hoztunk! Szerencsénkre legalább leírtuk, így nyoma marad.) Először is válasszuk ketté azt, amink most van:
<?php // SANITY CHECK assert
function shrlink($url, $max)
{
    if ( strlen($url) > $max )
        $url = shrink_str($url, $max);

    assert ('strlen ($url) <= $max');
    return "<a href='$url'>$url</a>";
}

function shrink_str($string, $max, $mark = '...', $where = 0)
{
    assert ('is_string($string)');
    assert ('is_numeric($max) && $max > 0');
    assert ('is_string($mark)');
    assert ('is_numeric($where) && abs($where) < strlen($string)');

    return $string;
}
?>
A shrink_str() függvényt lokális függvénynek szánjuk, azaz nem szeretnénk, hogy a shrink modul képzeletbeli interfészén megjelenjen, és más modulok közvetlenül ezt hívják. Gyors és egyszerű függvénynek készítjük el, amit védett környezetben használunk: csakis a modulon belül. Ezt C-ben egyszerűen a fordítóra bízhatjuk a static kulcsszó megadásával, PHP-ban azonban a static-ot nem használhatjuk függvényekre csakis változókra.
Mindezt azonban kis kerülővel PHP-ben is szimulálhatjuk:
<?php // STATIC assert
function shrink_str ( /* ... */ ) {
    // static kulcsszó szimulálása függvényekhez
    assert ('($dbg = debug_backtrace()) && !strcmp($dbg[0]["file"], $dbg[1]["file"]');
    // ...
}
?>
Ez a rövid, de mágikus sor lekérdezi az aktuális függvényhívási láncot, és összehasonlítja az assert() függvény hívási helyét ($dbg[0][]), tehát a lokális függvényt, és az eggyel felette levő függvény helyét. Ez a "sztringbe zárt" kód az assert()-en belül fut, emiatt tartalmazza a backtrace az assert() helyét, és ezért van lehetőségünk mindezt megtenni. Ezt a nem túl szemet gyönyörködtető sort célszerű szöveges állandóba menteni, vagy kis módosítással külön függvénybe, és ezt átadni paraméterként az assert()-nek:
<?php // lokális függvények elérésének ellenőrzéséhez előre definiált állandó:
define ('STATIC_FUNCTION', '($dbg = debug_backtrace()) && !strcmp($dbg[0]["file"], $dbg[1]["file"]');
// vagy segédfüggvény:
function STATIC_FUNCTION() {
    $dbg = debug_backtrace();
    return !strcmp($dbg[1]["file"], $dbg[2]["file"]);
}

function shrink_str ( /* ... */ ) {
    // csak a vele egy file-ban definiált függvények hívhatják
    assert (STATIC_FUNCTION);
    // vagy segédfüggvénnyel:
    assert ('STATIC_FUNCTION()');
    // ...
}
?>
Itt érdemes megjegyezni, hogy konstanst használó megoldásnál nem mindegy milyen formában adjuk át a feltételt. Az első cikkből emlékezhetünk, hogy célszerű string paramétereket használni. Ebben az esetben csak a fenti megoldás vezet célra, mert a STATIC_FUNCTION értéke már maga string típusú. A segédfüggvényes megoldás előnye az általa kiváltott PHP hibaüzenet egyszerűségében van. Hasonlítsuk össze ezeket:

PHP Warning: assert(): Assertion "($dbg = debug_backtrace()) && !strcmp($dbg[0]["file"], $dbg[1]["file"])" failed in .../assert_static.php on line 14.
PHP Warning: assert(): Assertion "STATIC_FUNCTION()" failed in .../assert_static.php on line 16.
A különbség önmagáért beszél.

Ezután a kis kitérő után vegyük szemügyre a szétválasztás során felvett újabb feltételeket. Ezek a leggyakoribb felhasználási módját példázzák ennek a függvénynek, amikor azt vizsgáljuk, hogy a kapott paraméterek megfelelnek-e a józan észnek. Emlékezzünk vissza, nem akartuk teletömni lokális függvényünket felesleges feltételvizsgálatokkal és hibakódok generálásával, legalábbis nem olyanokkal, amelyek minden meghívásnál lefutnak. Gyors, egyszerű és hatékony függvényt akartunk. A fejlesztés során viszont szükség van/lehet a paraméterek ellenőrzésére, ezért arra az időre használjuk az assert()-et.

Nézzük át, hogy miről mesélnek nekünk ezek a feltételek! Az első és a harmadik egyszerű típusvizsgálat, magától értetődő. A második azt fogalmazza meg, hogy a visszaadandó, rövidített $string-nek a hosszára pozitív számot várunk. Az utolsóban pedig újabb tervezési döntést fektetettünk le. A $where paramétert a substr() és substr_replace() függvénynél ismert módon fogjuk értelmezni, azaz, pozitív szám esetén a $string elejétől, míg negatív érték esetén pedig annak végétől számított eltolást jelenti. Később, ha már eleget teszteltünk, egyszerűen lehetőségünk van kikapcsolni az assert() feltétel vizsgálatokat. Ezzel gyorsítjuk is a programunkat, hiszen a kódok egy részét nem kell futtatni. A forrásfájl elemzésekor fellépő többletidőt természetesen nem tudjuk ezzel a módszerrel csökkenteni, de így is jelentős lehet a megtakarítás.

Ideje most már tényleg kidolgozni a funkciókat (tételezzük fel, hogy tényleg szükségünk van rá :)).
<?php // FUNCTIONALITY assert
function shrink_str($string, $max, $mark = '...', $where = null)
{
    assert ('is_string($string)');
    assert ('is_numeric($max) && $max > 0');
    assert ('is_string($mark)');
    $strlen = strlen($string); // ezt az erteket majd kesobb is hasznaljuk!

    assert ('is_numeric($where) && abs($where) < $strlen');

    if ( strlen($string) < $max )
        return $string;
    if ( empty($where) )
        $where = (int)$max/2;

    if ( $where > 0 )
        $string = substr_replace($string, $mark, $where, $where - $max);
    else
        $string = substr_replace($string, $mark, $max + $where - $strlen, $where);

    assert ('strlen($string) == $max');
    return $string;
}
?>
Nem teljesen jó a megvalósítás. Most azonban a cikk eredeti témájánál maradva egy apró hibára hívom fel a figyelmet! A $where alapértelmezett értékét 0-ról null-ra változtattam, amivel nyilván valami célom volt a függvény írása során, de elfelejtettem ezt leírni, sőt a hozzátartozó feltételt aktualizálni. Az első néhány teszt futtatása után ez felszínre is kerül:

Warning: assert(): Assertion "is_numeric($where) && abs($where) < $strlen" failed in .../shrink.php on line 16

Ezután valószínűleg a return előtti assert()-be fogunk belebotlani, mivel a $mark hosszával nem korrigáltuk a substr_replace() paramétereit. (Kíváncsi vagyok, hány embernek szerepelt ez kitétel a specifikációs listáján, amit a cikk elején kellett összeírnunk. Bizony, sok feltételezés kimondatlanul marad, ami megnehezíti később a dolgunk.) Ezt mindenki egyértelműnek gondolja az elején, de mégis elkövettem ezt a hibát. Az ilyen jellegű assert()-ek arra valók, hogy a nem triviális műveletsorozatok végeredményének helyességéről meggyőződjünk, jelen esetben, hogy a függvény teljesítette-e a feladatát. A shrink_str() további csiszolása már nem tartozik szorosan e cikksorozat témájához, ezért azt más alkalommal folytatjuk.

Térjünk vissza az eredeti shrlink() függvényhez végezetül! Erről azt mondtuk, hogy jó lenne, ha meg lehetne megadni, hogy a link mely részeit részesítse előnyben a rövidítés során a függvény. Ezzel lehetővé válna, hogy pl. a fórumokon belüli, belső linkeknél az URL elejéből is (host + path) csíp le karaktereket ezzel lehetőséget teremtve arra, hogy a belső útvonalból minél nagyobb rész látható maradjon, míg külső linkeknél csak a host utáni részeket csökkenti. Ezt közelíti az alábbi megoldás, ami a parse_url() függvényre épít. Az algoritmus a lehető legegyszerűbb módon számolja a csökkentés méretét, amin jócskán lenne mit finomítani.
<?php // CYCLE assert
function shrlink($url, $max, $from = 'path')
{
    assert ('is_string($url)');
    assert ('is_numeric($max) && $max > 0');
    assert ('is_string($from)');

    if ( ($urlen = strlen($url)) < $max )
        return "<a href='$url'>$url</a>";

    // megkeressuk azt a URL reszt, ahonnan a roviditest elkezdhetjuk
    $parts = parse_url($url); $len = 0;
    while ( (list($key,$val) = each($parts)) && $key != $from )
        $len += strlen ($val);
    // van ilyen elem?
    assert ('$val');
    // mar ennyivel kevesebb hely all rendelkezesre
    $max -= $len;
    do { // ezutan kovetkezo elemeket fokozatosan roviditjuk, amig beleferuk $max-ba
        $parts[$key]  = shrink_str($val,(1 - $len/$urlen)*strlen($val), $mark);
        $lval = strlen($parts[$key]);
        $max -= $lval; $len += $lval;
    } while ( (list ($key,$val) = each($parts)) && 0 < $max );

    assert ('$max <= 0');
    return "<a href='$url'>".glue_url($parts).'</a>';
}
?>
Az assert() használatára azonban újabb példát látunk ebben a függvényben; most ciklus utáni állapotot vizsgálunk vele. Az első ciklus keresi meg azt az URL részt, amitől kezdve az URL-t rövidíteni lehet, aminek a nevét paraméterként kapja a shrlink() függvényünk. Ha alaposabban megvizsgáljuk a környező kódot, rájöhetünk, hogy ezt tömörebb formában is megadhattuk volna - assert('array_key_exists($from, $parts)'); -, azonban az első ciklusra enélkül is szükségünk volt. Ráadásul ez sokkal jobban kiemeli azt a tényt, hogy a második ciklus feltételezi a $val változó beállított értékét, hiszen első teendője ezt továbbadni a shrink_str()-nek.

Általános gyakorlat nagyobb, nem megfelelően struktúrált kódok átalakításakor assert()-eket elhelyezni, amivel ellenőrizni lehet, hogy az átalakítás nem okozott-e funkcionális változást.
Nemrégiben egy 900-1000 soros függvényt kellett átalakítanom, ahol úgy tűnt, a while ciklust le lehetett cserélni do ... while-ra. A biztonság kedvéért ezt a következőképpen tettem meg:
<?php // REFACTORING assert
$p = $hash[$i][head];
while ($p != -1 ) {
    // ... a maradék ~900 sor
    $p = $entries[$p['next']]['head'];
}

$p = $hash[$i][head];
// itt elotesztelo ciklus volt eredetileg, de vajon ugyanolyan jo-e a hatulteszelos?
assert ($p != -1 );
do {
    // ...
    $p = $entries[$p['next']]['head'];
} while ( $p != -1 );
?>
Ugyanezen átdolgozás alkalmával erősödött meg a gyanúm, hogy rengeteg felesleges változót - és így erőforrást - használ el ez a kód. A kód tüzetes átolvasása után ilyen sorokkal tűzdeltem meg a forrást: assert ('$p_entry === $currEntry'), hogy igazoljam a feltevést.

Tovább az úton

A vezérlőszerkezetek cseréje, nagyobb kódrészletek átalakítása, és a duplikált hivatkozások felderítése csak egy-egy újabb példája annak, hogy hogyan és mennyiféle célra használható ez az egyszerű függvény főleg PHP-ban, ahol igen bonyolult összefüggéseket is nagyon tömören meg lehet fogalmazni. E cikk keretében nem érintettük azokat az eseteket, amikor erőforrások - állomány leírók (file handle-k), adatbázis-erőforrások meglétéről vagy jellegéről kell biztosítani egy adott kódrészletet. Ezek valójában nem is jelentenek újdonságot, hiszen nagyon hasonló a kezelésük az itt bemutatott egyszerű, skalár értékek vizsgálatához.
Az assert() használatához azonban elengedhetetlen a tesztelés, a kód futtatása, hogy valóban életre keljenek az így dokumentált peremfeltételek és feltételezések. Ehhez azonban jól megválogatott és jó mennyiségű tesztadatra is szükség van. A következő részben erről is szólunk.
 
1

gratula :)

Anonymous · 2005. Jan. 5. (Sze), 15.00
Szia!

Ez egy nagyon hasznos cikk szerintem, remélem lesz folytatása is. Jó munkát mindenkinek.
2

Velemeny

durexhop · 2005. Jan. 29. (Szo), 17.49
A cikk jo... Es van ertelme, csak az en meglatasom szerint egypar kimenetet is bele lehettet volna tenni... :} Az assert tenyleg egy jo fuggv. csak ha programon belul hasznalod...[A felhasznalotol bejovo adatok "eros ellenorzese" utan, ha mar tuti hogy a felhasznalo jo adatott adott meg...] vagyis a programok belso reszeben..., de OOP kivetel osztalyok irasaval ez is megoldhato :} csak a logikat kell megirni+Zend motorba belenyulni hogy mindig kivetel valtodjon ki .... :}
3

Idő

sayusi · 2005. Szep. 17. (Szo), 16.18
Ma szántam rá pár órát és átolvastam a cikkeket, majd csináltam teszt kódokat és próbálgattam. Nagyon tetszik.

"Bízzál Istenben és tartsd szárazon a puskaport!" -Cromwell