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!
■
másra számítottam
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.
Miert?
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.
DIC
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.
Hasznaljak
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.)
Manual type hinting
Igy van
Azért különböztessük meg a
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 :)
DIC + interfész?
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! :)
Semennyire
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.
Köszi a cikket, nagyon hasznos
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.)
Nagyon megszívlelendő
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. egyfindBy...
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).Ilyenkor a @return
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.
Hát ha valamit nem lehet a
list($result, $success) = findByName('foo');
is olvashatóbb megoldás szerintem.Hát ha egy PHP függvény
list()
a legjobb megoldás, csak teljesen megfeledkeztem róla (nem használok PHP-t nagyon rég).NotFound
A PHP-nál nagyságrendekkel
Egy kivételnél egyébként per definitionem semmi nem töri el váratlanabbul a kód működését :)
Szerintem ez az alaptalanabb
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 :-)
Én nem PHP-ról, de még csak
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.Nem hiszem
A C++ eleve nem szóba don't
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.
A kivételkezelésben ugye a
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.
És mert exceptiont csak
Épp arról szól a szál, hogy kivételes eset-e a nem létező kulcs egy cache-ben.
Kontextusfüggő
Általában híve vagyok annak,
A null jelölése?
Egész eddig azt hittem, hogy ezt így is kell csinálni. :)
Szerintem így konzisztensebb
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:
@return stdClass|null
Jogos
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.
komment
Re: komment
@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ó.kódkiegészítés
Ez ok.
|null
-t vagy ne.Amúgy egyre inkább azt látom, hogy semmi sem szól ellene, így miért ne!
Kerdes
Amiben ugye az a nagy szívás,
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.
Legkisebb meglepetes elve
DI vs ServiceLocator
Példa DI-ra:
Ugyanez ServiceLocatorral:
Martin Fowlernek van egy elég jó kis cikke IoC, DI, ServiceLocator témában, akit bővebben érdekel.
Like
nagyon like :)
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:
Imho a helyes implementálás
Az annotálás a Doctrine miatt
Ja vágom. Láttam már olyat
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.
Az annotálást sokan
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).