Tegye fel a kezét, aki még nem bizonytalanodott el abban, hogy mikor nyúljon absztrakt osztályhoz és mikor interfészhez! Annak ellenére, hogy a különbséget általában mindenki érti vagy érteni véli, mégis néha felcserélhetőnek tűnik a kettő. És ez pedig csak bonyolódott PHP-ben, mióta megjelentek a trait-ek, amik sok esetben nagyon vonzónak tűnnek. Ezekről mind írtam 2011-ben egy összefoglaló cikkben, amit szintén érdemes elolvasni, de vajon mikor, melyiket kell alkalmazni a gyakorlatban? Ezt igyekszem tisztázni ebben a cikkben.
Tartalom
- Bevezető
- Absztrakció és a problémák felosztása
- Interfész
- Absztrakt osztály
- Trait
- A választás menete
- Utószó
Bevezető
Szeretek elgondolkodni olyan megoldásokon, amik első pillantásra értelmetlennek tűnnek. Ennek az az oka, hogy szeretem tudni, hol vannak a határok. Bele akarok futni azokba a hibákba, amik miatt elmém legmélyebb gyökereiben rögzül, hogy mit, miért nem csinálunk, illetve mikor lehet mégis egy trükköt bevetni, ami normális esetben programozói eretnekség. Az a helyzet, hogy a programozónak rengeteg eszköz áll rendelkezésére. Sok konkrét problémára több alternatíva is lehet. Egyes alternatívák nem sokban különböznek egymástól. Mondhatni, ízlés dolga, melyiket választom. Másokat viszont már jobban meg kell fontolni. Azt hiszem, ilyen ennek a cikknek is a témája.
Ha azt mondom, interfész, akkor az emberek többségének két dolog jut eszébe róla. Az, hogy csak publikus metódusok fejlécét lehet benne definiálni és az osztályok öröklésével ellentétben több interfészt is implementálhat egy osztály. Ez persze szép és jó, de ez valójában csak a lehetőségeket írja le és nem azt, hogy ezekkel a lehetőségekkel hogyan és mikor kell élni. Lássuk tehát, ahogy én látom! Mindenek előtt viszont azt kell tisztázni, miért van szükség bármelyikre is.
Absztrakció és a problémák felosztása
Ha nekiállsz egy programnak, nem osztályokat tervezel, nem metódusokat alkotsz, hanem elsősorban meghatározod a program célját, majd azt, hogy kik (vagy mik, ha programkönyvtárról van szó) fogják használni a programot, és nekik milyen szolgáltatásokra van szükségük. Innentől már könnyebben felismerhető, hogy a programban milyen entitásokra, modellekre van szükség, amik mondhatni, önálló életet élnek majd. Ezek az entitások képességekkel (metódus) rendelkeznek majd, amik definíciójának helyet kell találni és állapotaik (tulajdonság) lesznek, amiket szintén valahol tárolni kell.
Az ember és főként a programozó nem szereti ismételni önmagát, ezért megpróbálja a problémákat általánosítani, egy általános megoldást találni rá, majd az általános megoldásokat egy speciálisabb esetben felhasználni, illetve kiterjeszteni. Az általános megoldásoknak pedig az igényei is általánosak. Nem tudom és nem is akarom tudni, hogy pontosan milyen elemmel fogok dolgozni, csak azt, hogy annak milyen képességei vannak. Olyan képességei vannak-e, mint amiket én az adott helyzetben fel akarok használni. Persze, megvizsgálhatnám minden alkalommal, hogy rendelkezik-e a megfelelő paraméterezésű metódusokkal a kapott objektum, de ez nagyon nem lenne hatékony. Itt jönnek képbe a típusok. Mert tudom (legalábbis erősen következtethetek rá), hogy egy adott típusú objektum metódusai egy adott problémakört fednek le és nem csak azonos paraméterekkel rendelkeznek.
Egy objektum típusát pedig gyakorlatilag az alábbi tényezők határozzák meg:
- milyen osztály példánya az objektum
- az objektum osztálya milyen további osztályok leszármazottja
- az öröklési láncban szereplő osztályok milyen interfészeket implementálnak
Ezeket a típusokat vagy feltételben vizsgálva, vagy pedig type hinting-et alkalmazva már a metódus leírásában kiköthetem, hogy milyenféle objektumokkal hajlandó dolgozni. Igaz, ez nem garancia arra, hogy a programozó alávaló módon ne valósítsa meg az absztrakt típusnak teljesen ellentétesen a saját metódusait, de nem is ez a cél.
Ezek ismeretében az is lehetséges, hogy egy osztály nem örököl egy másik osztálytól, hanem annak a bizonyos másik osztálynak egy példányát várja paraméterként akár már a konstruktorában. Ez a függőség, azaz angolul dependency és ezt a megoldást nevezzük Dependency Injection-nek. Ha a saját osztály maga hozná létre a függőségének példányát, az nem egy absztrakt megoldás lenne. Nem lehetne lecserélni a függőséget egy másik implementációra. Kivéve persze, ha maga az osztály lenne a paraméter, de ez más problémákat vethetne fel és paramétert sem sikerül spórolni vele.
Van még más lehetőség is, de egyelőre ezen a ponton nem szeretném lelőni a poént. Most inkább megmutatom az alternatívákat részletesebben.
Interfész
Ismertetés
Az interfésznek a feladata, hogy olyan metódusok leírását tartalmazza, amiket bármely osztály, ami paraméterként várja az objektumot, meghívhat. A "bármely" szóból adódik, hogy csak publikus metódusokat lehet itt definiálni. Azaz, ha egy osztály implementál egy interfészt, akkor csak publikus metódusokkal fog bővülni. Az interfésznél tehát PHP-ben két lehetőség van.
- Nem adod meg a láthatóságot, ami így alapértelmezésben publikus lesz.
- Megadod direkt módon a "public" kulcsszóval, hogy publikus a metódus.
Tehát egy rendkívül egyszerű interfész valahogy így néz ki:
interface StringifyInterface
{
public function toString(): string;
}
Előfordulhat, hogy egy bizonyos képességhez, tehát metódushoz különböző variációk tartoznak. Olyanokra gondolok itt, mint hogy nem csak stringgé akarok alakítani valamit, hanem azt is meg akarom mondani, hogy milyenné. A fenti példánál maradva mondhatnám, hogy legyen egy rövid és egy hosszú verziója. De elképzelhető, hogy alapértelmezett értéket kötök ki. Ilyenkor lehet értelme az interfészben konstansokat is deklarálni. Én azonban eddig ezzel nem gyakran éltem.
interface StringifyInterface
{
public const FORMAT_JSON = 'json';
public const FORMAT_PLAIN = 'plain';
public const FORMAT_YAML = 'yaml';
public const DEFAULT_FORMAT = self::FORMAT_PLAIN;
public function toString(string $format = self::DEFAULT_FORMAT): string;
}
Ebben a példában persze annak nincs sok jelentősége, hogy az alapértelmezett formátumot feltüntettem a metódus leírásában, mivel csak a paraméterek számát, illetve azok típusát köti meg az interfész. Egyes IDE-k viszont ezt a leírást átmásolják az implementáció fejléceként is, így azzal már nem kell ott foglalkozni. Ráadásul a példa is egyszerűbb volt így... Most lebuktam, ez volt a fő ok.
Az interfészek örökölhetnek is más interfészektől. Akár egyszerre többtől is. Ez nem olyan öröklődés, mint az osztályok esetén. Itt ugyanis még nincsenek implementációk. Ezért sem az "implements" kulcsszót kell az interfészek láncolásához használni. Ha két interfész rendelkezik azonos nevű metódussal, de minden tulajdonságukban is megegyeznek ezek a metódusok, akkor ezek egymással teljesen kompatibilisek. A metódust persze csak egyszer kell majd implementálni. Ezzel, ha tudod, hogy az osztályaidnak több interfészt is implementálnia kell, egy közös interfészre egyszerűsítheted a rájuk hivatkozást.
Van egy speciális fajtája az interfészeknek, amik se konstanst, se metódusfejlécet nem tartalmaznak, de még csak nem is örökölnek. Az ilyen interfész semmi másra nem jó, mint olyan típust rendelni egy osztályhoz, ami ellenőrizhető, és így gyakorlatilag egy egyszerű flag-ként funkcionál, jelezve, hogy valamilyen szempontból ezeknek az osztályoknak a példányait hasonlóan kell kezelni. Például naplózni kell őket. De Java nyelvben ilyen a Serializable interfész. Ez a Marker Interface Pattern, vagy magyarul "Jelölő interfész minta", amit már volt alkalmam használni, de az igazat megvallva megfelelő tervezéssel ezek elkerülhetők és már arra sem emlékszem, hol vetettem be. Lehet, hogy ez egy tudat alatti védekezés, nehogy el tudjam árulni a hibáimat...
Értékelés, javaslatok
- Mit tud az interfész?
- Publikus metódusok fejlécének gyűjteményét tartalmazhatja. Az implementáló osztálynak ezekkel kompatibilis fejléceket kell definiálnia.
- Tetszőleges számú további interfésztől örökölhet, amíg az ősinterfészek metódusfejlécei kompatibilisek egymással és az öröklő interfész metódusaival is. PHP 5.3 előtt nem definiálhattak azonos metódusokat különböző implementált interfészek.
- Tetszőleges számú interfészt implementálhat egy osztály is, de az előző pontban írt kompatibilitási feltétel ebben az esetben is érvényes.
- Publikus konstansokat tartalmazhat, de ezeket a konstansokat sem az implementáló osztály, sem a leszármazott interfész nem írhatja felül.
- Hogy szoktak rá hivatkozni: Gyakran szerződésként hivatkoznak rá, de nem teszik hozzá, kik a szerződő felek. Talán az implementáló osztály és az osztály metódusait hívó elem között lehetne érteni, de ez sosem segített hozzá az interfészek megértéséhez.
- Hogy lehet még jellemezni: Az interfész tulajdonképpen egy TODO lista. Egy lista a megírandó nyilvános metódusokról, egy kis extrával a konstansok személyében.
- Mikor írj interfészt:
- Ha több különböző implementációt szeretnél egy probléma megoldására, vagy
- ha lehetőséget szeretnél adni arra, hogy más különböző implementációt valósítson meg.
- Mikor ne írj interfészt:
- Ne írj interfészt csak azért, mert egy osztályban szükséges bizonyos publikus metódusokat elérhetővé tenni! Az egyetlen implementáció típusának megkövetelése ilyenkor éppen elég az azt felhasználó metódusokban.
- Ha egy osztályban privát vagy védett metódus létét szeretnéd megkövetelni, abban az interfész nem segít. A továbbiakban erre is mutatok megoldásokat.
- Bár PHP-ben statikus metódus is megkövetelhető egy interfész által, ennek általában nincs értelme. Az interfésznek leginkább pont az a lényege, hogy egy paraméterben átadott objektumról el lehet dönteni, hogy azon bizonyos metódusokat meg lehet hívni. A statikusan hívott metódusnál általában úgyis tudod, melyik az az osztály, amin a metódust hívod. Ez alól csak az az eset kivétel, amikor a metódust dinamikusan hívod például call_user_func függvénnyel. Ilyenkor viszont érdemes más megoldást keresni. Java nyelven nem is lehet statikus metódust definiálni egy interfészben.
- Mikor implementálj egy interfészt:
- Ha van egy olyan, általad használni kívánt metódus vagy függvény, ami megköveteli azt a típust, vagy
- ha lehetőséget akarsz adni arra, hogy mások átadhassák annak a metódusnak az objektumot és persze,
ha ennek van is értelme.
- Mikor ne implementálj: Csak azért ne implementálj interfészeket, mert pont olyan metódusokat hozol létre az osztályodban, ami megegyezik az interfész metódusaival! Az egyezés nem feltétlenül jelenti azt, hogy az interfészt a te céljaidra írták. Egy add() metódus például összeadhatná a paraméterben átadott értékeket, de egy listához is hozzáadhatná őket.
Absztrakt osztály
Ismertetés
Az absztrakt osztály legfőbb feladata, hogy olyan általános, absztrakt megoldásokat tartalmazzon, amik az objektum saját adatain, állapotain dolgoznak. Ezen a ponton viszont nem mindig lehet a problémát teljes egészében megoldani. Maradnak még olyan részek, amiket rá kell bízni a leszármazott osztályokra. Az, hogy az absztrakt osztályban lehetnek olyan metódusok, amiket nem fejtesz ki, ennek következménye. Definiálod a metódust annak paramétereivel, ezzel bebiztosítva az osztályt, hogy a metódus majd létezni fog, amikor meghívnád.
Ennek az absztrakt metódusnak viszont nem kell publikusnak lennie, de nem lehet privát. Vagy publikus (public) vagy védett (protected), hiszen a privát metódusokat a leszármazott sem éri el. Tehát egy absztrakt metódus vagy csak azért készül, hogy az absztrakt ős hivatkozhasson rá, ezzel áthárítva a megvalósítás felelősségét a gyermek osztályra, vagy azért, hogy a funkció kívülről is elérhető legyen az objektumon. Előfordulhat, hogy az ősosztálynak nincs szüksége a metódusra. Ekkor viszont, főleg több metódus esetén, érdemes megfontolni az interfész alkalmazását, mivel gyakorlatilag semmi sem fogja kötni az ősosztályhoz a funkciót, ami ezek szerint azért lett nyilvános, mert szükség van a nyilvánosságára és nem csak egy extra lehetőség a nyilvánosság számára.
Érdekesség, hogy statikus metódus is lehet absztrakt. Felteheted a kérdést, hogy ebben mégis mi az érdekes. Nos, sem a Java, sem a C#, de még a C++ sem támogatja az absztrakt és a statikus jelzőt egyszerre. Statikus híváshoz ismerni kellene az osztálynak a pontos nevét és nem csak egy őstípust. Ehhez vagy dinamikusan kellene tudni összeállítani az osztály és metódus nevét, amit majd valami, valahol, valahogyan meghív, vagy az ősnek kellene tudnia hivatkozni az utolsó leszármazottra, úgy, ahogy a szülőre is tud. Ez nem olyan triviális. Már ahol egyáltalán lehetséges. PHP-ben viszont az 5.3-as verzió óta a "static" kulcsszó használható a "self" helyett is, ami így nem az aktuális osztályra, hanem a leszármazottra hivatkozik. Ezen kívül dinamikusan is könnyedén előállítható egy meghívandó metódus. Persze az, hogy valamire van lehetőség, még nem jelenti azt, hogy tanácsos is élni vele.
Értékelés, javaslatok
- Mit tud az absztrakt osztály?
- A "class" kulcsszó előtti "abstract" kulcsszóval absztraktnak definiálható egy osztály, amit így önmagában nem lehet példányosítani, csak örökölni.
- Egy osztály tartalmazhat olyan metódusokat, amiknek nincs törzse, csak fejléce a paraméterekkel és típusdefiníciókkal. Ilyenkor viszont a metódus definíciója elé ki kell tenni az "abstract" kulcsszót. Absztrakt metódusok esetén pedig az osztályt is kötelező absztraktnak jelölni.
- Statikus absztrakt metódust is létre lehet hozni, de ezt érdemes lehetőség szerint kerülni. Ha mégis szükséges, akkor az ősben a "static" kulcsszóval kell a leszármazott statikus metódusára hivatkozni.
- Ez inkább nem tudás, de egyszerre csak egy absztrakt osztály örökölhető.
- Az absztrakt osztály is örökölhet absztrakt osztálytól, de nem kell megvalósítania a szülő metódusait. Azt csak az első nem absztrakt osztálynak kell megtennie.
- Tartalmazhatja interfészek implementációjának jelölését, de nem kell ténylegesen implementálnia is azokat. Csak az első nem absztrakt osztálynak kell ezt megtennie.
- Definiálhat ugyanolyan fejlécű absztrakt metódust, mint amilyen egy implementálandó interfészben van,
de nem definiálhat olyant, ami egy ősosztályban már absztraktnak lett jelölve. - Az interfészekkel ellentétben tulajdonságokat is tartalmazhat.
- Hogy szoktak rá hivatkozni: Osztály, ami nem példányosítható, de örökölhető és lehetnek absztrakt, kifejtetlen metódusai.
- Hogy lehet még jellemezni: Talán szerencsésebb az absztrakció felől megközelíteni a kérdést. Egy ilyen osztály tartalmazhat absztrakt, kifejtetlen metódusokat és ennél fogva természetesen példányosítani sem lehet. Nincs értelme. Nem kötelező azonban, hogy ténylegesen tartalmazzon is absztrakt metódust. Egy általános metódusgyűjteményként is szolgálhat, ami az objektum adatain dolgozik, miközben magára az absztrakt ősre nincs szükség önálló példányként. Ahogy az állat objektumra sem feltétlen van szükség, ha létezik "kutya", "macska" stb... objektum is.
- Mikor írj absztrakt osztályt:
- Ha szükséged van általános metódusokra az objektum adatainak kezelésére és ehhez elég az egyetlen ősosztály öröklésének lehetősége. Ez persze még egy hagyományos osztállyal is megoldható lenne. A példányosítás tiltása nem egy nélkülözhetetlen előny.
- Ha egy leprogramozandó probléma nagy részét meg tudod oldani általánosan, de van konkrétabb típustól függő rész, ami azokban vár megvalósításra. Ez azt is jelentheti, hogy csak védett metódusokra van szükséged.
- Mikor ne írj absztrakt osztályt:
- Felesleges olyan absztrakt osztályt írni, ami semmi mást nem tartalmaz, mint publikus absztrakt metódusok fejléceit, esetleg konstanst. Ezt ugyanis egy interfész is tudja, ráadásul abból több is implementálható, míg absztrakt osztályból csak egy.
- Ahogy üres interfészt, úgy üres absztrakt osztályt sem érdemes írni.
- Ahogy interfészt sem, absztrakt osztályt sem érdemes csak a létezésükért írni. Ha nincs szükség különböző implementációra, akkor gyakorlatilag ezzel csak több részre van osztva az osztály annak terhére, hogy a megvalósításokat több helyen is kell keresni. Az IDE-k többsége képes a metódusokat becsukni, ha zavaróan sok van. Ha fájlméretben nőtt meg az osztály, akkor az is lehet, hogy eleve nem is egy osztályba valók azok a funkciók, hanem több, egymással azonos rangú, konkrétabb területért felelős osztályokba.
- Mikor örökölj egy absztrakt osztályt: Ha annak célja megegyezik a saját osztályodéval és szükséged van az általa nyújtott legtöbb metódusra is, akkor nyugodtan örököld.
- Mikor ne örökölj egy absztrakt osztályt: Ha néhány metódus hasznos, de a legtöbbet meg kellene változtatnod, akkor esélyes, hogy nincs szükséged arra az osztályra. Mivel egy PHP osztály forrása nyilvános,
így akár a szükséges részeket át is lehet emelni saját osztályba. Természetesen figyelembe véve a license-t és mindig illik az eredeti szerzőt kommentbe beírni. Később saját magadnak is hasznos információ lesz.
Trait
Ismertetés
PHP-nél egy "include"-dal behúzott fájl esetén azt szoktuk mondani, hogy olyan, mintha annak forráskódját átmásolnánk abba a fájlba, amiben az include-ot írtuk (nem 100%-ig igaz persze). Ugyanezt mondhatjuk a trait-ekről is. A trait-nek abszolút semmi köze az osztályhoz, amiben alkalmazod. Type Hinting-re nem alkalmas. Bár, ha nagyon akarja az ember, lehet vizsgálni, hogy egy osztály használ-e egy trait-et, nem erre való. Előfordulhat viszont, hogy valamit komplikált megoldani pusztán örökléssel és egyébként sincs semmi szükség a köztes osztályokra. Az osztályoknak valójában semmi közük egymáshoz, de mégis van olyan műveletük, ami közös. Lehet, hogy sok ilyen kis "csomag" létezik előre elkészítve és nincs minden osztályban mindre szükséged, de több olyan is lehet, amiben több trait is hasznos. Ezeket a csomagokat más is elkészíthette. Ő tartja karban, javítja. Te pedig megkapod a puzzle darabkáit.
Bár lehetnek, akik szerint trait-re nincs szükség, én azt gondolom, mindent lehet a helyén kezelni. Nem szabad abba a hibába esni, hogy ha egy eszköz létezik, akkor már mindent azzal akarunk megoldani, de lehetnek olyan esetek, amikor egy eszköz többet segít, mint amennyit esetleg árt, ha egyáltalán árt. A Symfony keretrendszerhez írt legnépszerűbb modulok is alkalmazzák ezt a megoldást. Mutatok egy példát is olyan esetre, amire nem nagyon látok egyszerűbb lehetőséget, habár nem nélkülözhetetlenek hozzá a trait-ek. A példa a Singleton (egyke) mintát mutatja be:
{
final private function __construct() { }
final private function __clone() { }
public function __sleep()
{
throw new \RuntimeException(__CLASS__ . ' is not serializable!');
}
abstract public static function getInstance();
}
class SingletonTest
{
use Singleton;
private $count = 0;
public function increase() { $this->count++; }
public function getCount() { return $this->count; }
public static function getInstance() { return new self(); }
}
A Singleton minta lényege, hogy egy osztályból ne lehessen több példányt létrehozni, csak egyetlen egyet. Ehhez minden olyan metódust le kell tiltani, ami új példány létrehozásához vezetne. Nyilvánvalóan ilyen a konstruktor, de nem mindenki gondol rá, hogy a klónozást sem szabad engedni. Ezen kívül, ha egy objektumot sorosítasz (serialize), majd visszaalakítasz (unserialize), azzal megkerülhető a konstruktor és klónozás sem történik, mégis lesz új példány. Klónozáskor viszont lefut a __sleep() mágikus metódus, így abban egy kivételt dobva a sorosítás is letiltható. Oké, a Singleton minta hasznáról lehet vitatkozni, de a trait jelentőségét azt hiszem, jól szemlélteti a példa. Teljesen nyugodtan használhattam benne privát jelzőket, és a getInstance() absztraktnak jelölésével kikényszeríthettem annak definiálását abban az osztályban, amibe úgymond bemásolta a trait is az ő absztrakt verzióját. Nyilván, ha valóban bemásoltam volna, nem definiálhattam volna kétszer ugyanazt a metódust, még akkor sem, ha az egyik absztrakt.
A Trait tehát egy trükkös jószág, mert bár típusként nem játszik szerepet, mégis képes arra, amire egy absztrakt osztály, mégpedig anélkül, hogy öröklésre lenne szükség. Így több, különböző trait is definiálhat absztrakt metódusokat, azaz egy picit interfészként is funkcionál azzal az extrával, hogy bármilyen metódust definiálhat. Nem csak publikusat.
Értékelés, javaslatok
- Mit tud a trait?
- Bármit tartalmazhat, amit amúgy egy osztály is, viszont nem értelmezhető rajta az öröklés vagy interfész implementálása.
- Egy osztály akárhány trait-et importálhat. Tehát bizonyos szempontból olyan, mintha több osztályt is lehetne örökölni.
- Bár nem öröklésnek hívjuk, de trait is importálhat további trait-eket. Így az interfészekhez hasonlóan több trait is hivatkozható egyetlen, azokat összefogó trait-en keresztül.
- Tartalmazhat absztrakt definíciót is, azt viszont az importáló osztály is megvalósíthatja. Nincs szükség leszármaztatásra.
- Metódusnevek egyezése esetén képes átnevezni azokat. Ez hasznos, de fontos szem előtt tartani, hogy ha más, a trait-en kívüli kódrészek hivatkoznak ezekre a metódusokra, nem fogják tudni az új nevet és nem érik el a metódust, vagy nem azt érik el, amire számítanak.
- Hogy szoktak rá hivatkozni: Egy olyan nyelvi konstrukció, amivel hasonló hatást lehet elérni,
mint a többszörös öröklődés, habár a PHP azt nem támogatja. - Hogy lehet még jellemezni: Felfogható egyfajta csomagként is, ami nem egy komplett osztályt tartalmaz, hanem annak csak egyes alkotóelemeit, mint egy kirakós játék. Ezekből az elemekből lehet építkezni, amit akár egy teljesen ismeretlen fejlesztő is karbantarthat.
- Mikor írj trait-et:
- Ha egymástól független, kisebb egységeket szeretnél újrafelhasználhatóvá tenni tetszőleges osztályokban.
- Ha olyan egységeket szeretnél újrafelhasználhatóvá tenni osztályokban, amik nem valósíthatók meg örökléssel. Ilyen például a privát metódusok beillesztése, de még inkább a final kulcsszó használata.
- Ha csökkenteni szeretnéd az osztályhierarchia bonyolultságát egyes, egyébként szükségtelen leszármaztatások megszüntetésével.
- Ha mások számára szeretnél önálló osztályként és főleg objektumként kevésbé elképzelhető, kisebb kódrészleteket osztályokban újrafelhasználhatóvá tenni.
- Mikor ne írj trait-et:
- Ha az öröklésnek van értelme és annak megvalósítása nem teszi a kódot átláthatatlanabbá,
mint amennyi előnnyel jár. - Csak saját kódba szánt, és egyébként is csak egy osztályban felhasználandó rövidebb egységek csoportosítására
- Ha az öröklésnek van értelme és annak megvalósítása nem teszi a kódot átláthatatlanabbá,
- Mikor használj/importálj egy trait-et:
- Ha sem az absztrakt osztály, sem az interfész nem tűnik jó választásnak, de a trait megoldja a problémád.
- Ha van egy használt osztálykönyvtár, ami biztosít számodra trait-eket az általa megoldandó problémára és meg is felelnek, logikusnak tűnik azt alkalmazni is. Persze itt is az a kérdés, többször van-e rá szükség és nem csak egy 2-3 soros kiegészítésről van-e szó.
- Mikor ne használj/importálj egy trait-et: Mivel öröklésben nem játszik, talán egy fokkal kisebb probléma egy trait-nek csak egy nagyon kis része miatt importálni azt, mint osztályt örökölni ugyanígy. A saját kód teleszemetelését azonban mindenképp érdemes elkerülni, mivel minden egyes hozzáadott sor növeli a hibák esélyét is és a kód átláthatóságán, tisztaságán is ront.
A választás menete
Ha a fentiekből még nem lett teljesen világos, mikor, mit használj, azt tudom javasolni, hogy először azt fontold meg, szükséged van-e egy modellnek különböző implementációira. Ha nincs, akkor valószínűleg egyik eszköz sem indokolt.
Ha szükséges több implementáció, akkor:
- Ha lesznek olyan publikus metódusai az objektumnak, amiknek elérését biztosítani kell más, független osztályoknak az implementációt nem ismerő metódusaiban, akkor azoknak a meghívandó metódusoknak interfészt készíthetsz. Logikusan különválasztva azokat, amiket nem mindig együtt kell implementálnia az osztályoknak.
- Ha van olyan művelet, ami minden implementációban közös, annak készíthetsz absztrakt osztályt. Ez nem jelenti azt, hogy akkor nincs szükség az előző pontban írt interfészre. Sőt, abból, hogy az interfészben definiálsz egy publikus metódust, nem következik, hogy az absztrakt megvalósításodban ne lehetne azt rögtön ki is fejteni. Ha ugyanis csak az absztrakt osztályban írnád le az érintett publikus metódust és az interfészből kihagynád, miközben más osztályokban az interfész típust vizsgálod, az az osztály erre a publikus metódusra a típuson keresztül nem tudna következtetni. Ha biztos akarnál lenni abban, hogy valami nem csak az interfészt implementálta, hanem a kihagyott metódust is tartalmazza, az absztrakt osztályod öröklését is vizsgálnod kellene például "instanceof" operátorral. Ha ezt nem teszed meg, az interfész implementálása sosem lesz elég. Mindig örökölni kell az absztrakt osztályt is, vagy valahonnan megsejteni, hogy milyen metódust kell még megírni.
- Ha pusztán örökléssel nem lehet egyszerűen megoldani a problémát vagy logikailag két osztály nem áll egymással szülő-gyerek viszonyban, akkor a fentiek helyett alternatív megoldást kell keresni.
- Ha egy bizonyos feladatkör elvégzésére érdemes egy önálló osztályt létrehozni és így különálló objektumpéldányokat kezelni, ugyanakkor az új osztállyal nincs szülő-gyerek viszonyban, akkor ezeket a példányokat Dependency Injection-nel átadhatod az osztályodnak.
Példa erre, amikor egy PDO objektumra van igény adatbázis kezelésére egy olyan osztályban, ami önmaga nem az adatbáziskezelő műveleteket egészíti ki. - Ha metódusok, tulajdonságok egy vagy több kisebb egységét szeretnéd különböző osztályokban viszontlátni, amik öröklés útján nem, vagy csak komplikáltan lennének megoldhatók, bevetheted a trait-eket. Itt is fontos, hogy a trait használata nem jelenti azt, hogy a trait által tartalmazott metódusokat ne specifikálhatná egy interfész vagy absztrakt osztály. Ahogy azt többször kiemeltem, a trait nem a típusok megkülönböztetésére és ezáltal a metódusok létének vizsgálatára való, hanem csak egyszerűen újrafelhasználható kódrészleteket biztosít.
- Ha egy bizonyos feladatkör elvégzésére érdemes egy önálló osztályt létrehozni és így különálló objektumpéldányokat kezelni, ugyanakkor az új osztállyal nincs szülő-gyerek viszonyban, akkor ezeket a példányokat Dependency Injection-nel átadhatod az osztályodnak.
Utószó
A megfelelő eszköz megtalálása nem mindig jön kisujjból. Gyakorlással és egy kis gondolkodással azonban rá lehet érezni a helyes útra legkésőbb akkor, amikor a választás után előjönnek a problémák, és kiderül, hogy talán másképp kellett volna dönteni. Bízom benne, hogy ezzel a cikkel valamennyire hozzá tudtam járulni ahhoz, hogy ezt megelőzd. Azt még meg kell jegyezzem, hogy a fentiek csupán általános javaslatok. Egy mankó azok számára, akik nem látják (remélhetőleg csak nem látták) a teljes képet. Az eszközök azért vannak, hogy kielégítsék az igényeinket. Ha egy eszköz megold egy problémát és még az után is optimálisabbnak tűnik, hogy megfontoltad az ajánlásokat, nincs mese, a saját utad kell járni. Elvégre senki sem képes mindent előre látni.
Ha hozzáfűzni valód van a cikkhez, kérdésed, javaslatod van, várom a hozzászólásokat itt és a facebook oldalon is.