ugrás a tartalomhoz

Don’t make me think! on PHP code

janoszen · 2013. Okt. 9. (Sze), 14.20
Don’t make me think! on PHP code

Aki kicsit is foglalkozott weboldalak használhatóságával, egész biztosan találkozott Steve Krug Don’t make me think című könyvével. A használhatóság (usability) egyetlen feladata, hogy a látogató vagy vevő számára a lehető legegyszerűbbé tegye a meghatározott cél elérését. Felmerül a kérdés, hogy vajon hasonló elveket, gondolatokat alkalmazva hatékonyabbá tehető-e a webfejlesztési munka, egész konkrétan a PHP-s fejlesztés.

Rengeteg féle-fajta rendszeren dolgozom, és igen gyakran nagy mélységben bele is kell nyúlnom a kódba. Akik személyesen ismernek, tudják, hogy átmegyek házisárkányba, ha az általam használni kívánt kód nem egyértelmű vagy bonyolult használni. Eljön az a pont, amikor a legzseniálisabb programozó sem tudja az összes függvény paramétereit megjegyezni. Jellemzően a legjobbaknál is vége van, ha félmillió kódsor fölötti projektet kell fejbe tartani. Plána, ha nem is egy projekten kell dolgozni.

Egy másik szempont a gondolatmenet megtartása. Ha picit figyeljük magunkat kódolás közben, általában egy adott cél lebeg a szemünk előtt, méghozzá egy adott funkcionalitás megvalósítása. Ehhez használjuk a már korábban megírt függvényeket. Ha viszont ezen függvények használata nem triviális, nem egyértelmű, akkor neki kell állnunk elolvasni a dokumentációt, vagy rosszabb esetben a függvény kódját. Ez megakasztja a gondolatmenetünket, ami semmiképpen nem hat jól a produktivitásunkra. Magamat figyelve azt vettem észre, hogy ha elég ilyen zavaró tényező lép fel, egy ponton túl egyszerűen képtelen vagyok a feladatra koncentrálni, hiszen a teljes időmet azon függvények bogarászása tölti ki, amiknek az én munkámat kellene segítenie.

Függvények írása

Éppen ezért az első és szerintem legfontosabb a függvény öndokumentáló jellege. Nézzük az alábbi példát:

/**
 * @param iPlugin $plugin
 * @param string  $eventName
 * @param int     $priority
 */
public function register(iPlugin $plugin, $eventName, $priority = 10) {}

Mint látható, a függvénynek nem adtam szöveges dokumentációt. Noha lehetne magyarázni a függvény működését, ennyiből már ki kell derülnie, hogy nagyságrendileg mire szolgál. Ha ez nem adott, akkor már csökkent a kódolás sebességén, hiszen a triviális feladatok elvégzésére is bele kell túrnunk a doksiba vagy rosszabb esetben a függvény kódjába ahhoz, hogy használni tudjuk.

Itt kitérnék a magas szintű angoltudás fontosságára. Sajnos rengetegszer látok magyarból, németből, kínaiból tükörfordítással előállított függvényneveket. Amennyiben úgy döntünk, hogy a céges nyelvünk az angol, akkor fordítsunk időt az önképzésre, hiszen senkinek nem jutna eszébe a fenti függvényt mondjuk insert() néven keresni. Ez megint egy olyan pont, ami lassíthatja a függvényeinkre épülő kódok írását. Amennyiben nem vállalható az angol nyelvű függvényelnevezés, még mindig jobban járunk a magyar nyelvű nevekkel a tükörfordított függvénynevek helyett.

A példára visszakanyarodva, érdemes megnézni a függvény paramétereit is. Mint látható, egyértelműen dokumentálva van, hogy mit vár a függvény. Ahol szöveget vár, ott string van írva, ahol számot, ott int. A kiszámíthatóság szempontjából a lehető legrosszabb, ha ilyeneket írunk: int|bool. Ez esetben ugyanis nem triviális, hogy miért és milyen esetekben kell int-et, milyen esetekben bool-t átadnunk.

Ugyanilyen főbenjáró bűn szerintem a szöveggel indexelt tömbök átadása. Ha tehát a $plugin változó egy tömb lenne, amely bizonyos jól meghatározott elemein vár különböző adatokat, akkor a függvényt használni kívánó fejlesztőnek vagy el kell olvasnia a függvény dokumentációját, vagy még rosszabb esetben át kell túrnia a függvény kódját.

Adatkonténerek

Ezekre az esetekre szervezhetünk adatkonténer osztályokat avagy data objecteket. Ezek semmi mást nem tesznek, mint bizonyos adatokat eltárolnak szabványos formában. A fenti projektből véve egy példa erre:

abstract class Event implements iEvent {
    /**
     * @var string
     */
    protected $name;
    
    /**
     * @param string $name
    */
    public function setName($name) {
        $this->name = $name;
    }
    
    /**
     * @return string
     */
    public function getName() {
        return $this->name;
    }
}

Mint látható, az Event osztály egyetlen egy paramétert tartalmaz, méghozzá az esemény nevét. Az objektum tetszés szerint bővíthető további attribútumokkal, és ha korszerű fejlesztőkörnyezetet használunk, az ún. getter és setter függvényeket automatikusan legenerálja az osztályba.

Ha igazán ki akarunk tenni magunkért, akkor definiálhatunk egy interfészt is. Az interfész ugyanis lehetővé teszi azt, hogy bárki írjon olyan osztályt, ami megvalósít bizonyos szabványos metódusokat, azonban azt ne kelljen egy közös őstől (pl. az Event-től) származtatni. Ennek akkor van értelme, ha a különböző implementációk radikálisan különböznek vagy különbözhetnek egymástól. Az Event osztályunkra írt iEvent interfész így néz ki:

interface iEvent {
    /**
     * @param string $name
     */
    public function setName($name);
    
    /**
     * @return string
     */
    public function getName();
}

Nem egy bonyolult művelet megírni, azonban a későbbiekben sokat segíthet a felület szabványosítása. Mivel interface-t használunk, nem kötjük a függvényünket egyetlen implementációhoz sem, bárki írhat bármilyen eseményt megvalósító osztályt, amit átadhat a függvényünknek.

Felmerül a kérdés, hogy mi a teendő akkor, ha több elemet akarunk átadni? Természetesen építhetünk egy csomagoló osztályt az ArrayObject nyomán, azonban ez talán egy picit sok. Viszont a korszerű IDE-k itt is kínálnak egy egyszerűen használható megoldást, méghozzá az objektumtömb dokumentációt, itt egy változóra alkalmazva:

/**
 * @var iPlugin[]
 */
protected $plugins = array();

Mint látható, itt egyértelműen egy tömbről van szó, azonban szabványosítottuk azt, hogy milyen elemek lehetnek a tömbben. Innentől kezdve egy okos IDE például egy foreach ciklusban fel fogja nekünk kínálni az adott objektumhoz tartozó függvényeket kódkiegészítésben. Noha az egyértelmű függvény specifikáció rendkívüli módon segíti az implementáló dolgát, érdemes lehet azért egy típus ellenőrzést végrehajtani az elemeken, ha félő, hogy más is kerülhet az adott tömbbe. (Én személy szerint az itt leírt elvek betartása mellett elenyészően kevés ilyen hibával találkoztam éles kódban.)

Visszatérési értékek

Ha visszatérési értékekkel dolgozunk, akkor is érdemes a korábban leírtakat fontolóra venni. Véleményem szerint a lehető legrosszabb dolog, amit tehetünk a következő:

/**
 * @return bool|SomeObject
 */

Mint látható, a függvény visszatérési értéke vagy SomeObject lehet vagy pedig egy boolean érték, legtöbbször false. Ezzel több súlyos probléma is van. Egyrészt a programozó lusta, és akármennyire is verjük nádpálcával, akkor sem fog minden egyes függvény visszatérést ellenőrizni. Ebből következően, ha objektumként próbálja használni a visszatérési értéket, a program azonnal elhasal. Másrészt megint csak gondolkozni kell azon, hogy milyen esetekben lehet false a visszatérési érték, lassítjuk a programozási folyamatot, megtörjük a gondolatmenetet.

A konfliktus feloldására két megoldásunk van. Az első, hogy a hibaesetet egy kivétel (exception) dobásával jelezzük. Ez aztán szépen felfele kipörög a függvényből, amíg el nem kapja valaki. Az előnye az, hogy nem kell feltétlenül minden hívó félnek visszatérési értéket ellenőriznie és adott esetben tovább adnia, hanem mondjuk 3-4 szinttel feljebb is elkapható a hiba:

/**
 * @return SomeObject
 * @throws ConnectionException ha csatlakozasi hiba tortent.
 */

Ez egy elég egyértelmű dokumentáció, ha csatlakozási hiba történt, ez az exception fog jönni. Kérdés nincs, egy pillantás elég ahhoz, hogy megállapítsuk, kell-e foglalkoznunk az üggyel.

A másik megoldás, ha rá akarjuk kényszeríteni a hívó függvényt az eredmény kezelésére, hogy a visszaadott objektumba implementálunk egy sikerességet jelző mezőt, például így:

class SomeObject {
    /**
     * @var bool
     */
    protected $success;
    
    /**
     * Megmondja, hogy a muvelet sikeres volt-e.
     *
     * @return bool
     */
    public function getSuccess() {
        return $this->success;
    }
    
    /**
     * @param bool $success
     */
    public function setSuccess($success) {
        $this->success = $success;
    }
}

Ezzel egyértelműen jeleztük a hívó félnek, hogy az eredményben van egy mező, amivel foglalkozni kell, tehát sokkal kisebb az esélye, hogy elsiklik a kezelése felett.

DI Container/Helper

Ha ezen mind túl estünk, már csak egy részlet maradt hátra, a DI Container pattern. Nagyon leegyszerűsítve a DI Container működését, van egy osztályunk, amely különböző egyéb osztályokat tárol kulcs szerint. Ha tehát példának okáért egy helpereket (segítő osztályokat) biztosító DIC-ről van szó, a HTML helpert így érhetnénk el:

$this->getHelper('html')->doSomething();

Namost, ez a kódrészlet szerintem ezer sebből vérzik. Honnan tudjuk, hogy a 'html' sztringet kell oda beírni? Honnan tudjuk, hogy milyen objektumot kapunk vissza? Ki garantálja, hogy annak az objektumnak lesz doSomething() metódusa?

Az első és legfontosabb dolog, amit tehetünk (illetve amire rávehetjük az általunk használt framework gyártóit), hogy készítsenek interfészeket a leggyakrabban használt helperekhez, szolgáltatásokhoz, DIC elemekhez stb. Ha lehet, ez az interfész tartalmazza konstansként a kulcsot, amivel meg lehet hívni, például így:

interface iHTMLHelper {
    const HELPER_NAME = 'html';
    
    public function doSomething();
}

A második lépés pedig az, hogy az általunk használt helperekre, szolgáltatásokra stb. gyártsunk kódkiegészítést lehetővé tevő függvényeket, például kiterjeszthetjük a DIC osztályt, és adhatunk hozzá egyéni függvényeket:

class MyProjectDIC extends FrameworkDIC {
    /**
     * @return iHTMLHelper
     */
    public function getHTMLHelper() {
        return $this->getHelper(iHTMLHelper::HELPER_NAME);
    }
}

Amennyiben esetleg PHP 5.4-et használunk, az egyes helperek ezeket a függvényeket akár traitek formájában rendelkezésre is bocsájthatják, hogy a saját DIC / helper osztályunkba csak be kelljen húzni.

Mindez minimális befektetés egy projekt elején, viszont napi szintű szívásokat és megakadásokat előz meg.

Alkalmazás meglevő projektekre

Ez mind szép és jó, de valószínűleg kevés ember dolgozik zöldmezős projekten, így az itteni elvek alkalmazása igen nehézkes. Éppen ezért érdemes egy szabályt bevezetni a csapatban: aki egy kódrészlethez hozzányúl, rendbe rakja. Ez sokszor nem triviális, és sok esetben nem is fog 100%-osan megtörténni, azonban már egész kis erőfeszítéssel nagyon komoly előrelépést lehet elérni a mindennapi munkák tempójában.

Ezen a ponton külön kiemelném a PHPStorm fejlesztőkörnyezetet, amely ugyan fizetős, de igen komoly eszközkészletet ad a PHP fejlesztő kezébe, például egy valóban működő refaktor (függvény- és osztályátnevező) eszközt, felhívja a figyelmet a hibás, illetve adott esetben problémát jelentő kódokra, és még sok minden mást is.

Végszó

Rossz kódot bármilyen programnyelven lehet írni, talán nem szégyen egy picit tanulni a szigorú(bb)an típusos nyelvektől vagy a usability szakeremberektől.

Támogatod? Ellenzed? Kérdésed van? Mondd meg a véleményed a kommentekben!

 
janoszen arcképe
janoszen
Pásztor János a fejlesztés és az üzemeltetés témakörével is foglalkozik, igyekszik az egyikben szerzett tapasztalatokat a másikba is átültetni. A Weblaboron kívül Facebookon ,Twitteren, a Refaktor Magazinon és a Refactor Zone-on publikál.
1

másra számítottam

blacksonic · 2013. Okt. 9. (Sze), 15.13
Hasznos cikk, eleje nagyon tetszett.

A cím és első pár sor elolvasása után egy DomainDrivenDesign és CleanCode keverékére számítottam, nekem a végén a DI kicsit kilógott az előtte felhozott példák sorából.

Visszatérési értéknél lehet még NullObjectPatternt használni.
2

Miert?

janoszen · 2013. Okt. 9. (Sze), 15.19
Valoban kilogott a DIC a peldakbol, de kivancsi vagyok Neked miert? En azokat a dolgokat igyekeztem osszeszedni, amik szemely szerint engem meg szoktak akasztani a napi PHP-s munkaban.

Ami a NullObjectPatternt illeti, azzal inkabb ovatosan. En sokkal inkabb hive vagyok az explicit hibamegjelolesnek, hiszen az ilyen "implicit" (null object) visszaadas megint csak nem osztonzi arra a fuggveny hasznalojat, hogy hibaellenorzest vegezzen.
3

DIC

blacksonic · 2013. Okt. 9. (Sze), 15.32
DIC csak annyiból lógott nekem ki a sorból, hogy amik előtte voltak példák azok tényleg növelték a kód érthetőségét, egy DIC plusz absztrakciót hoz be, ami jó, de az érthetőséghez nem tesz hozzá úgy mint az előző példák. Kód karbantarthatóság szempontjából viszont kötelező.

A NullObjectPatternt csak ötletnek hoztam, szerintem a hiba jelzésének kezelésében valamilyen konszenzusnak kell kialakulnia a kódbázison belül és következetesen egy félét használni, mert különben csak ront a kód értelmezhetőségén.
4

Hasznaljak

janoszen · 2013. Okt. 9. (Sze), 15.37
Sokan hasznalnak DIC-et vagy valamilyen szoveggel indexelt helpert es napi szinten belefutok abba, hogy nincs code completion, sot sokszor nem is tudom, hogy melyik stringgel indexelt helpert kellene hasznalni es ez minden nap elegge faj.

Nyilvan kisebb projekteknel, ahol mondjuk van 5 helpered es mindegyiket ismered fejbol (esetleg nem csapatban dolgozol), akkor ez az egesz nem problema. Ha viszont (nagyobb) csapatban dolgozol, tobb projekten, tobb szazezer, esetleg millio sorral, akkor nem tudod fejben tartani, ugyhogy hagyatkozol a kodkiegeszitore. Ha nincs kodkiegeszito, tursz es kozben elfelejted, mit akartal.

Amit bemutattam, egy lehetseges megoldas, nyilvan vannak mas otletek is. (Hint: tessek rola kommentelni vagy blogolni.)
5

Manual type hinting

complex857 · 2013. Okt. 9. (Sze), 15.59
A legtöbb IDE vagy autocomplete script ismeri a következő comment formátumot (pl phpstorm is):

/* @var $foo FooClass */
$foo = bar();
Nyilván nem hosszútávú megoldás ha mindenhol kézzel megmagyarázni a gépnek, hogy mit is lát, illetve ugyanúgy hajlamosak elévülni mint más commentek, de ideális ha csak ragtapasz jellegű megoldásra van szükséged.
6

Igy van

janoszen · 2013. Okt. 9. (Sze), 16.00
Igy van, ezt szoktam is neha hasznalni amikor muszaj, de eppen ezert nem is irtam bele a cikkbe, mert hack.
28

Azért különböztessük meg a

szjanihu · 2013. Okt. 10. (Cs), 18.31
Azért különböztessük meg a DIC-et meg egy egyszerű, szöveggel indexelt objektum konténert, a kettő nem ugyanaz még akkor sem, ha az utóbbi esetleg képes dinamikusan példányosítani valamilyen kötött formátumnak megfelelő osztály(oka)t.

DIC-et az alkalmazásban szinte sehol sem szabadna látni, azt az esetek 99%-ában el kellene fednie a keretrendszernek. Jómagam Symfony DIC-et használok (nem Symfonyval, és ugye PHP-ről van szó), és az alkalmazások belépési pontjain kívül sehol nem jelenik meg. Még a ZF1-es controllerekbe is kívülről, automatikusan injektálom be phpdoccal ellátott memberekbe, amiket így aztán az IDE is tud értelmezni és dobálja az autocompletet.

Egyébként ha lenne PHP-ben generics, akkor kb. egycsapásra megoldódna az összes ilyen jellegű probléma, de az akkor már nem is PHP lenne :)
7

DIC + interfész?

T.G · 2013. Okt. 9. (Sze), 16.45
Remek cikk, viszont a DIC téma nekem is kilóg. :)

Mennyire elterjedt a DIC esetén az extra interfészek? Nem tudom megfogalmazni, de valahogy ez nekem nem kerek. A saját függvényem miért nem a konkrét típust adja meg?
Nehézkesnek látom, hogy a külső fejlesztő létrehozzon nekem ilyen interfészt, illetve ha én hozom létre, akkor meg egy felesleges hibaforrást adtam hozzá a rendszerhez.
De mondom ezt úgy, hogy nem biztos, hogy teljesen átjött, hogy mit szerettél volna! :)
8

Semennyire

janoszen · 2013. Okt. 9. (Sze), 16.52
Semennyire nem elterjedt, pont ez a bajom. Az interface szerepe itt nem csak az, hogy eltaroljuk benne a DIC kulcs nevet, hanem hogy szabvanyositja az adott DIC kulcs alatt mindenkor eltarolasra kerulo osztaly funkcionalitasat. Maskepp szolva az interface egy igeret, hogy a megadott modon fog mukodni az ott lako service.

A PHP-s vilagban az interfacek hasznalata eroteljesen alulreprezentalt (talan egy kulon bekezdest meg is erdemelt volna a cikkben), de baromi jo dokumentacios eszkoz, hiszen ha modulok kozotti atadasokhoz absztrakt osztraly helyett interfacet hasznalunk, pontosan a modulok kozotti laza kapcsolatot teremtjuk meg.

A sajat lekerdezo fuggvenyed termeszetesen a konstans helyett hasznalhat stringet is, ez mar igazan nem ront sokat a dolgon, de stringeknel en mindig annak a hive vagyok, hogy lehetoseg szerint pontosan egy helyen szerepeljen a kodban, igy konnyu modositani rajta.
9

Köszi a cikket, nagyon hasznos

Pepita · 2013. Okt. 9. (Sze), 20.18
A PHP-s vilagban az interfacek hasznalata eroteljesen alulreprezentalt (talan egy kulon bekezdest meg is erdemelt volna a cikkben)
Vagy akár kettőt... "Erőse(bb)en" típusos nyelveknél meg leginkább kötelező, nem is tudod kihagyni.

Mindenesetre az interface-ek használatára felhívtad a figyelmem, PHP-ben ilyen téren igen ellustultam - mert megengedi a lustaságot. Sajnos.

Az viszont már a keretrendszeredtől függ, hogy a helper osztály-e, vagy fv-gyűjtemény. (CodeIgniter megkülönbözteti a helpert a "lirary"-tól, ezért helpert én nem is igen írok. Viszont ezek a fv-ek elérhetők view-ban is, pont ez a különbség lényege.)
10

Nagyon megszívlelendő

tgr · 2013. Okt. 9. (Sze), 23.01
Nagyon megszívlelendő tanácsok. Phpdoc type hinting téren azt is érdemes tudni, hogy ha valamiért mégis ArrayObjectet (vagy valami más iterálható osztályt) használunk, akkor is meg lehet adni ArrayObject|Foo[] szintaxissal, hogy milyen elemek vannak benne.

Interfészeket használni egyebek mellett azért is jó, mert elrejti kódkiegészítésnél a felesleges dolgokat. Pl. ha dependency injectiont használunk, akkor általában tele vannak az osztályaink setSomeDependency(SomeDependency $someDependency) jellegű metódusokkal, amikkel az adott osztályt felhasználó kódnak semmi dolga nincs; az interfészben ezek nincsenek benne, így aztán a kódkiegészítésnél sem látjuk őket.

Az @return bool|SomeObject hibakezelésnél nyilván nem jó megoldás (erre találták ki a kivételeket), de mi van, ha hiba nélkül is előfordulhat, hogy nem kapunk vissza objektumot? (Pl. egy findBy... függvény, ami az adatbázisból kiszedi az első, a keresési feltételeknek megfelelő objektumot, de néha nem talál semmit; vagy egy cache get.) Ilyenkor a @return SomeObject|null-nál jobb megoldás nem nagyon van PHP-ben szerintem (funkcionális nyelvekben elegáns eszköz ilyenkor az Option type, de PHP-ben csak nagyon fájdalmasan adható vissza).
11

Ilyenkor a @return

Joó Ádám · 2013. Okt. 10. (Cs), 01.49
Ilyenkor a @return SomeObject|null-nál jobb megoldás nem nagyon van PHP-ben szerintem (funkcionális nyelvekben elegáns eszköz ilyenkor az Option type, de PHP-ben csak nagyon fájdalmasan adható vissza).


Több visszatérési értékkel elegánsan megoldható: az elsődleges egy null objektum, a sikerességet pedig egy referenciaként átadott változóba lehet írni.
16

Hát ha valamit nem lehet a

tgr · 2013. Okt. 10. (Cs), 09.42
Hát ha valamit nem lehet a dokumentáció olvasása nélkül értelmezni, akkor azok az argumentumaikat módosítgató függvények. Akkor már a list($result, $success) = findByName('foo'); is olvashatóbb megoldás szerintem.
22

Hát ha egy PHP függvény

Joó Ádám · 2013. Okt. 10. (Cs), 11.54
Hát ha egy PHP függvény argumentumlistájában referenciát látok, arról elég egyértelmű, hogy kimeneti változó. De tényleg a list() a legjobb megoldás, csak teljesen megfeledkeztem róla (nem használok PHP-t nagyon rég).
12

NotFound

janoszen · 2013. Okt. 10. (Cs), 02.09
Én erre az esetre definiáltam egy NotFoundExceptiont. Nem töri el váratlanul a kód működését és szépen el lehet kapni.
13

A PHP-nál nagyságrendekkel

Joó Ádám · 2013. Okt. 10. (Cs), 02.51
A PHP-nál nagyságrendekkel jobb teljesítményt nyújtó nyelvekben sem javasolják, hogy vezérlési szerkezetként (tehát nem kivételes esetben) használj kivételt, mert sokkal-sokkal lassabb, mint egy normál visszatérés. Nem véletlen, hogy semmilyen könyvtári gyűjtemény semmilyen nyelvben nem használja ezt a megoldást.

Egy kivételnél egyébként per definitionem semmi nem töri el váratlanabbul a kód működését :)
17

Szerintem ez az alaptalanabb

tgr · 2013. Okt. 10. (Cs), 09.43
Szerintem ez az alaptalanabb PHP mítoszok egyike. Egyrészt igenis használják PHP-nél nem lényegesen gyorsabb (viszont lényegesen összeszedettebb) nyelvekben is vezérlésre, pl. a StopIteration Pythonban (amit át akar venni a Harmony is; StackOverflow-n van egy jó összefoglalás arról, hogy miért okosabb ez, mint a PHP megoldása), és viszonylag színvonalas PHP frameworkokben is előfordul (pl. a 404 és hasonló hibastátuszok Symfony2-ben belsőleg úgy vannak kezelve, hogy dobsz egy kivételt a kontrollerben; a FuelPHP Validator kivételeket dob).

Másrészt az az állítás, hogy a kivételkezelés lassú, a szó semmilyen gyakorlati relevanciával bíró értelmében nem igaz: egy kivétel létrehozásának, eldobásának és elkapásának is mikroszekundum nagyságrendű költsége van. Ha százezerszer csinálod egy requestben, akkor megérzed a különbséget a visszatérési érték alapú vezérléshez képest, de ha százezerszer kell kivételt dobnod egy requestben, akkor ott valami jóval nagyobb probléma van :-)
23

Én nem PHP-ról, de még csak

Joó Ádám · 2013. Okt. 10. (Cs), 12.24
Én nem PHP-ról, de még csak nem is szkriptnyelvekről beszélek. Annak idején elég sokat keresgéltem a témában, találni mindenféle értekezést, előadást C++ és Java vonalon a kivételkezelésről, általában fordítók fejlesztőitől. A kivételkezelés elég sok kód generálásával jár (ez nyilván általában érdektelen), és alig észrevehetően lassítja a kódot. A kivétel dobása és kezelése viszont már igen drága.

Ami a hivatkozott mérést illeti, ez így teljesen dilettáns, egy jobb fordító az ilyen faék eseteket egész biztos, hogy nyom nélkül kioptimalizálja. De még ha a PHP-ban benne is marad a kivételkezelés, épp a lényege, a stack visszapörgetése nem jelenik itt meg, mert nem lép át egy függvényhatárt sem.

A százezer pedig talán kicsit nagy szám, de ha mondjuk kollekciókban használod a most szokásos null helyett, akkor azért elég sok dobás összejöhet egy rendesen megírt kódban is.
24

Nem hiszem

janoszen · 2013. Okt. 10. (Cs), 12.39
Nem hiszem, hogy a webkiszolgalasban sok jelentossege van. Gyakorlatban sokkal fajdalmasabbak a WTF bugok mint mondjuk 10%-kal tobb CPU fogyasztas.
29

A C++ eleve nem szóba don't

tgr · 2013. Okt. 10. (Cs), 20.31
A C++ eleve nem szóba don't make me think műfajban :-) (Friss cikk a témában: Edward C++Hands) Javában lehet, hogy lassú a kivételkezelés, de ebből más nyelvekre még nem következik semmi.

Egy teszt négy egymásbaágyazott függvényhívással:


Szóval ezer kivételnél kezd érezhető (10ms) tartományban járni a különbség még egy ilyen publikus virtuális platformon is (rendes szervergépen nyilván elfér még pár nagyságrend). Ekkora gyorsulásért biztos nem áldoznám fel a kód olvashatóságát.
31

A kivételkezelésben ugye a

BlaZe · 2013. Okt. 10. (Cs), 21.46
A kivételkezelésben ugye a stacktrace összenyalábolása kerül sokba, objektum példányosítás van, aminek értelemszerűen nem 0 az időigénye. Ez minden nyelvben lényegesen drágább művelet, mint nem dobni exceptiont.

Mindezektől függetlenül kb a kutyát nem érdekli mennyibe kerül, mert hibakeresésnél, supportnál olyan infókkal szolgál, amiket egy if sosem fog nyújtani. És mert exceptiont csak kivételes esetben dobunk. Ha exceptiont dobunk, akkor nem az fog izgatni, hogy mennyi időbe fog kerülni a dobása, hanem hogy mennyi idő alatt és hogyan lehet eltüntetni a logból, és újra működővé változtatni az adott szolgáltatást.

A témából egy érdekes (java is, magyar is, blah :)) post Verhás Pétertől.
32

És mert exceptiont csak

Joó Ádám · 2013. Okt. 10. (Cs), 21.54
És mert exceptiont csak kivételes esetben dobunk.


Épp arról szól a szál, hogy kivételes eset-e a nem létező kulcs egy cache-ben.
34

Kontextusfüggő

BlaZe · 2013. Okt. 10. (Cs), 22.13
Ez elég kontextusfüggő. Nálunk pl az, dobunk is exceptiont. Több ilyen része van az alkalmazásnak, amin dolgozunk. Ha pl egy DIC-ben nincs benne egy keresett objektum, az egyértelműen kivételes eset, az alkalmazás működésképtelenségét jelzi. Ha egy előre feldolgozott adatot keresünk, ami újra legyártható, hát akkor kisnyúl, legyártjuk újra, max warnolunk egyet a logba, ha arra számítottunk volna hogy ott van, és megyünk tovább. Amit el lehet mondani, hogy mindegyik megoldásnak megvan a maga felhasználási területe. Egy biztos, a return null-ra nagyon jó ok kell legyen, mert az jellegénél fogva teljesen máshol boríthatja fel a kódot, mint ahol a lekezelése kellett volna történjen, és annál nagyobb WTF-ot nem lehet okozni egy kódban. Főleg ha nem szinkron hívások láncáról beszélünk, hanem bonyolítjuk kicsit a dolgot, és pl cache-en, több szálon keresztül terjedve vágja bele a kisbaltát a rendszerbe valahol.
19

Általában híve vagyok annak,

tgr · 2013. Okt. 10. (Cs), 09.50
Általában híve vagyok annak, hogy kivételkezelést használjunk a potenciális hibaforrást jelentő váratlan eseményekre, de ebben az esetben szerintem nagyon olvashatatlan kódot eredményez.

$user = $userCache->get('foo');
if (!$user) {
    $user = $userRepository->findByName('foo');
}
if (!$user) {
    throw new UserNotFoundException('foo');
}
vs.

try {
    $user = $userCache->get('foo');
} catch (NotFoundException $e) {
    try {
        $user = $userRepository->findByName('foo');
    } catch (NotFoundException $e) {
        throw new UserNotFoundException('foo');
    }
}
nem beszélve az olyan esetekről, amikor not found esetén egyszerűen null-ra kell állítani az adott mezőt, mondjuk egy modellnek a kapcsolt modellre való hivatkozását, és valami egészen más kód valahol egészen máshol kell hogy végezze a hibakezelést. Az Option tud adatszerkezetben utazni, és ott kell lekezelni, ahol végül felhasználod; a kivétel nem tud.
14

A null jelölése?

T.G · 2013. Okt. 10. (Cs), 08.44
Ez érdekes, én gyakran használom azt a megoldást, hogy megadom a visszatérés típusát, és oda kell érteni, hogy lehetséges, hogy az null, de azt típus megjelölésben nem tüntetem fel.

Egész eddig azt hittem, hogy ezt így is kell csinálni. :)

/**
 * @return Component A keresesi feltetelnek megfelelo elem, ha nincs talalat, akkor null.
 */
Ezért hasznosak ezek a gondolatébresztő cikkek, mert sokszor triviális dolgokat mások teljesen másként csinálnak, nem mondom, hogy ezután mindenhol fel fogom tüntetni a null-okat, de legalább tudom, hogy mások szerint fel kellene. :)
15

Szerintem így konzisztensebb

tgr · 2013. Okt. 10. (Cs), 09.15
Szerintem így konzisztensebb a type hint-tel: add(Component $component) - $component itt nem lehet null. Illetve nem tudom, hogy van-e jelenleg PHP-hez olyan kódelemző eszköz, ami kiszúrja, hogy egy nem biztonságos (potenciálisan null) változón hívunk meg valamit, de így legalább az esélyük megvan rá :-)

A még félkész PSR draft egyébként ezt írja:
This type is commonly used in conjunction with another type to indicate that it is possible that nothing is returned.
@return stdClass|null
18

Jogos

T.G · 2013. Okt. 10. (Cs), 09.48
Ha az a cél, hogy a kód használóját minél jobban segítsük, akkor ez az öt karakter valóban segítség. Elsőre inkább azt mondom, hogy ez nekem most furcsa, de mindenképpen van benne ráció.

Az okozza most nekem a bizonytalanságot, hogy a típusos programozást Java-val ismertem meg, ott meg csak egyféle típust adhatunk meg, de ettől függetlenül null értékkel is visszatérhetünk, azzal nincsen gond. Emiatt nekem a null-lal való visszatérés belefér feltüntetés nélkül.

Ám most gyorsan belenéztem néhány PHP-s keretrendszerbe és ez valóban gyakori jelölés.
20

komment

blacksonic · 2013. Okt. 10. (Cs), 11.29
Ezzel az a baj, hogy kommentet csak végső esetben olvas a felhasználó, viszont a kódkiegészítést annál inkább és IDE is az orrunkba nyomja.
21

Re: komment

T.G · 2013. Okt. 10. (Cs), 11.41
NetBeans-t használok, mielőtt ide írtam azért megnéztem, hogy pontosan mi a jelenség, a @return Component|null esetén kódszerkesztés közben nincs változás. Pont ugyanaz a hatás, mint a @return Component esetén. Természetesen a függvénylistában megjelenik, de azt meg csak akkor látom, ha az adott fájlt szerkesztem. Azaz eddig is használtam olyan komponenseket, amelyek feltüntették a null-t, ám mégsem lehet azt mondani, hogy az orromba lett volna nyomva ez az infó.
25

kódkiegészítés

blacksonic · 2013. Okt. 10. (Cs), 15.09
a kódkiegészítésre gondoltam, hogy azt nyomja az orrunkba ellentétben a kommentekkel
26

Ez ok.

T.G · 2013. Okt. 10. (Cs), 16.31
Azzal nincs is gond, én is dokumentációs kommentet írtam. Amit az IDE meg is ért. A kérdés számomra csak az, hogy használjam a |null -t vagy ne.
Amúgy egyre inkább azt látom, hogy semmi sem szól ellene, így miért ne!
27

Kerdes

janoszen · 2013. Okt. 10. (Cs), 17.02
Kerdes, hogy a
Call to a member function on a non-object
hibauzenet a logban mit mond egy ertelmesen megfogalmazott exception hibauzenetehez kepest.
35

Amiben ugye az a nagy szívás,

tgr · 2013. Okt. 10. (Cs), 23.20
Amiben ugye az a nagy szívás, hogy nullon metódust hívni fatal error, tehát nincs stack trace, és találgathatod, hogy pontosan hogyan is állt elő a hiba.
Ettől függetlenül a kivételdobás nem mindig megoldás, mert gyakran maga az adatstruktúra is olyan, hogy a null egy értelmes érték benne (pl. opcionális mezők), és akkor teljesen normális, hogy azt kapod vissza lekérdezéskor.
36

Legkisebb meglepetes elve

janoszen · 2013. Okt. 11. (P), 00.57
En a legkisebb meglepetes elve alapjan akkor adnek vissza nullt, ha a fuggvenytol elvarhato, hogy jo esellyel nem kapsz vissza ertelmes adatot (pl egy cache hivasnal). Mashol, ahol adatbazisbol kerdezel, azert inkabb azt varod, hogy jojjon valami ertelmes adat.
30

DI vs ServiceLocator

BlaZe · 2013. Okt. 10. (Cs), 21.21
A DI bár látszólag kilóg kicsit, viszont a cikk vezér gondolatmenetére, miszerint a kód dolgozzon a fejlesztő keze alá, illetve tegye magát olvashatóvá, követhetővé és karbantarthatóvá, abszolút beleillik. Én magát az injectiont hiányolom kicsit a példákból. Ez így felhasználástól függően lehet DI, vagy ServiceLocator pattern is, de inkább utóbbira hajaz. Mindkettő lényege, hogy laza contract legyen a felhasználó és felhasznált objektum között, utóbbi azonban ennek ellenére igényel egy erős függőséget, mivel maga a ServiceLocator (registry, context) szükséges az objektum működéséhez. Ez újrafelhasználásnál, vagy pl tesztek írásánál egy kis nehezítés lehet.

Példa DI-ra:

interface Service {
	function doSomething();
}

class ServiceImpl implements Service {
	public function doSomething(){
		echo 'doSomething';
	}
}

class NotServiceImpl{
}

class User {
	private $service;

	public function __construct(Service $service){
		$this->service = $service;
	}
  
	public function act(){
		$this->service->doSomething();
	}
}

// prints doSomething
$user = new User(new ServiceImpl());
$user->act(); 

// prints PHP Catchable fatal error:  Argument 1 passed to 
// User::__construct() must implement interface Service, instance 
// of NotServiceImpl given
$user = new User(new NotServiceImpl());
Ahogy a második hívásnál látszik is, a php is tudja kulturált hibaüzenettel honorálni a hibás argumentumot. Gondolom az IDE-k is tudnak rá kódkiegészíteni.

Ugyanez ServiceLocatorral:

interface Service {
	function doSomething();
}

class ServiceImpl implements Service {
	public function doSomething(){
		echo 'doSomething';
	}
}

class ServiceLocator {
	public static function getService() {
		return new ServiceImpl();
	}
}

class User {
	public function act(){
		ServiceLocator::getService()->doSomething();
	}
}

// prints doSomething
$user = new User();
$user->act(); 
Nyilván a DI-t még sokáig lehet tekerni annotációkkal, factorykkel stb, de az alapvető különbség a két megoldás között a fenti.

Martin Fowlernek van egy elég jó kis cikke IoC, DI, ServiceLocator témában, akit bővebben érdekel.
33

Like

janoszen · 2013. Okt. 10. (Cs), 22.09
Elsőként had mondjam el, hogy nagyon örülök, hogy konstruktív vita alakult ki a cikk nyomán. Ennek kapcsán nagyon szívesen olvasnék arról, hogy Ti, akik itt hozzászóltok, milyen problémás esetekkel találkoztok a napi munkátok kapcsán és hogy hogyan oldjátok meg. Nincs kedvetek beszállni a cikk írásba? Szívesen mentorálok is, ha kell.
37

nagyon like :)

EL Tebe · 2013. Dec. 10. (K), 10.43
Nagyon tetszik ez a cikk..

Az amúgy sokszor triviális alapoknál ("öndokumentálás") kell kezdeni, amit sokan kihagynak, kifelejtenek csupán azért, mert mondjuk nem kötelező része az éppen működésre bírandó programnak. Később nyilván nem fog belekerülni, ha már itt elmarad, aztán bogozza ki utólag akinek 6 anyja van..

Ha már egy kicsit megszokjuk ezeket a szép kommenteket, abból máris következik, hogy mondjuk egy Docrtine-nál nem kell behozni óriási lemaradásokat és talán nem fogunk megijedni egy ilyen láttán:
/** @Id @Column(type="integer") @GeneratedValue **/
Ez egy kötelező kör lenne a fejlesztőnek készülődő ifjú padawanoknak, akik most nézegetik az alapokat, ahogy a változó deklaráció, ez is egyfontos tétel.
38

Imho a helyes implementálás

inf · 2013. Dec. 17. (K), 18.06
Imho a helyes implementálás kb így megy: specifikáció -> api dokumentáció -> (unit teszt -> implementálás -> refaktorálás) ciklusban. Sokan csinálják úgy, hogy nekiállnak a specifikáció alapján kódolni, aztán kommentezik a kódot mint állat, és a végén generáltatnak egy dokumentációt, a tesztekre meg vagy marad idő vagy nem... Ez a phpdoc felannotálás szerintem egyedül arra jó, hogy segít az IDE-nek az automatikus kiegészítésben, mert a php gyengén típusos...
39

Az annotálás a Doctrine miatt

MadBence · 2013. Dec. 17. (K), 19.48
Az annotálás a Doctrine miatt kell, mert (sajnos :)) nem gondolatolvasó, segíteni kell neki, hogy rájöjjön milyen adatbázist kell felépítenie a háttérben.
40

Ja vágom. Láttam már olyat

inf · 2013. Dec. 17. (K), 21.03
Ja vágom. Láttam már olyat is, hogy dokumentációból generáltatták az egész adatbázist és api-t (osztályok, annotáció, stb.. nélkül), és utána csak kitöltötték a hézagot... :-) Elméletben lehetséges, gyakorlatban nem sokan élnek ilyesmivel...

Egy cikket egyébként tényleg megér az annotálás témája. Ha jól tudom doctrine-hez írtak külön engine-t ehhez. doctrine common annotations Vannak még más lib-ek is, de sokan erre esküsznek.
41

Az annotálást sokan

szjanihu · 2013. Dec. 23. (H), 01.49
Az annotálást sokan túlmisztifikálják, holott ez nem több, mint inline konfiguráció. A doctrine annotationst se nagy ördöngösség használni (mármint saját annotációt definiálni).

Egyébként tényleg annyira ködös tud lenni ez a téma, hogy amikor a DDD-ről tartottam előadást és kihangsúlyoztam a domain réteg függetlenségét, akkor mutogattak nekem a JPA annotációkra, hogy de bizony az ott van és no lám csak, mégse független. Holott az egész részletkérdés, mert ugyanazt ki lehet vinni külső fájlba (XML).