OOP - öröklött felület specializálódása
Sziasztok!
Ez egy nagyon egyszerű kérdés. De azért inkább azokhoz szól akik az OO-ban már jobban elmélyültek.
Adott egy absztrakt ősosztály, mely egy felületet definiál (ezen belül esetleg tartalmaz néhány sablonfüggvényt is, de ez mindegy). Ennek az osztálynak van egy metódusa, mely egy kapcsolódó (szintén absztrakt) osztály típust vár paraméterül. A leszármazott (példányosítható) osztály metódusa analóg módon már a kapcsolódó osztály leszármazottját várja paraméterül.
Hogy világos legyen, íme egy példa.
Adott a(Itt a
Ezek után az első konkrét megvalósítás ilyesmi lehet:Szóval ez mintha megszegné többek között a SOLID elveket. A probléma forrása, hogy a felületen absztrakt típusú paramétert határoztunk meg, így a
A kérdésem, hogy ennek ellenére vajon jó gyakorlat a fentebbi származtatási struktúra alkalmazása?
■ Ez egy nagyon egyszerű kérdés. De azért inkább azokhoz szól akik az OO-ban már jobban elmélyültek.
Adott egy absztrakt ősosztály, mely egy felületet definiál (ezen belül esetleg tartalmaz néhány sablonfüggvényt is, de ez mindegy). Ennek az osztálynak van egy metódusa, mely egy kapcsolódó (szintén absztrakt) osztály típust vár paraméterül. A leszármazott (példányosítható) osztály metódusa analóg módon már a kapcsolódó osztály leszármazottját várja paraméterül.
Hogy világos legyen, íme egy példa.
Adott a
Feldolgozo
nevű absztrakt osztály, melynek egyik metódusa a feldolgoz(). Ez a metódus egy Csomag
típusú paramétert vár:
abstract class Feldolgozo {
// ...
public function feldolgozMindent($csomagok) {
foreach ($csomagok as $csomag) $this->feldolgoz($csomag);
}
abstract public function feldolgoz(Csomag $csomag);
}
abstract class Csomag {}
feldolgozMindent()
az absztrakt függvényt használja fel; és most csak azért raktam be, hogy látszódjon, miért absztrakt osztályról beszélünk, és nem interfészről. Tulajdonképp ennek nincs jelentősége.)Ezek után az első konkrét megvalósítás ilyesmi lehet:
class MeglepetesFeldolgozo extends Feldolgozo {
public function feldolgoz(MegletesCsomag $csomag) {
echo "Meglepetés:\n\n";
$csomag->meglep();
}
}
class MegletesCsomag extends Csomag {
function meglep() {
echo "Tudom mit tettél tavaly nyáron!";
}
}
feldolgoz()
metódus a főosztály megvalósításaiban mindig csak a megfelelő mellékosztály példányát tudja fogadni, pedig a felületük ugyanaz. Itt tehát a felület nem biztosítja a behelyettesíthetőség elvét.A kérdésem, hogy ennek ellenére vajon jó gyakorlat a fentebbi származtatási struktúra alkalmazása?
Liskov
A konkrét problémára a válasz az, hogy a Feldolgozo csak annyit csinál, hogy lerakja a címzett elé a csomagot (átértelmeztem kézbesítőnek, így jobb :)). Az már a csomag felelőssége és belső működésének eredménye, hogy lány ugrik elő belőle, vagy fiú :)
Kicsit életszerűbben, egy közeli példával: Van egy postásod és egy levél. A levél lehet sima, ajánlott, tértivevényes stb. De a postásnak a levél csak levél. Ami a különbség neki, hogy a levélre ránézve tudja, hogy ez pl ajánlott, és alá kell írnia az átvevőnek kézbesítésnél, ezért odatol egy papírt, hogy írja alá. Ez OOP-re fordítva azt jelenti, hogy a levél típusa alapjan cselekszik a postás (if instanceof AjanlottLevel). Vagy még kulturáltabban, a levél definiál egy átadási protokollt, amit a postás végrehajt ($level->atadasModja()->vegrehajt()). A te példád itt úgy nézne ki, hogy lenne postás, aki csak ajánlott levelet hajlandó kézbesíteni, egyéb esetekben elkezd ordítozni az elosztóban, amíg a biztonságiak ki nem vonszolják :)
Remélem érthető volt.
Címzett és levél
Ennek ellenére még nem látok tisztán az ügyben. Talán azért, mert szerintem az én Feldolgozo osztályom inkább a címzettnek felel meg, nem a postásnak. Az egyes címzettek speciálisak aszerint, hogy milyen speciális levelet vesznek át. Az felület (átadás protokollja) ugyanaz, a paramétertípus (milyen levél) viszont változik.
Az eredeti problémám adatbázis-erőforrások burkolásánál adódott. Az igényeimnek megfelelő adatbázis absztrakciót szerettem volna készíteni. Kicsit ráértem mostanában, és ezt jó gyakorlatnak éreztem az OOP-ban való elmélyüléshez, illetve az egyes adatbáziskezelők mélyebb áttekintéséhez.
Az absztrakciós csomag két fő részből áll, az egyik a query bilder (ez most nem érdekes), a másik az erőforrások köré csoportosul. Utóbbi esetében olyan ősosztályok vannak, mint Database, Connection, Statement, Resultset, Transaction stb. Ezek absztrakt osztályok, és driverenként vannak leszármazottaik. És akkor ugyanott tartunk, mint a Feldolgozo és a Csomag esetében, azzal a jelentős különbséggel, hogy egyelőre nem volt arra szükségem, hogy a felületben kelljen megadnom driver-specifikus osztályt, bár ezt intuitíve csak "átmeneti szerencsének" éreztem. A Resultset például a konstruktorában várja a Statement objektumot, ráadásul gyakorlatilag a Statement gyártja. Ha PHP-ban létezne a visszatérési típus fogalma, ez akkor sem lenne baj, mivel a visszatérési típus finomítható (ha jól gondolom). Lehet, hogy elég arra törekedni, hogy minden ilyen típusos függőség a konstruktorban (vagy valami gyártófüggvényben, egyszóval létrehozáskor) menjen át, és ne jelenjen meg a felületben?
Az első példád a jobb
Köszönöm
Nem muszáj constructor
Típusok
Ha jól tudom (de lehet, hogy
Az IoC gyakorlatilag annyi,
Mégegyszer
MeglepetésCsomag
vagyMeglepetésCsomagSzoltáltató
. Illetve ezt a szolgáltatás dolgot tudtommal csak a legfelső szinten (üzleti logikában, ha úgy tetszik) használják, nem olyan mély rétegnél, mint az adatbáziskezelő (ha már megmondtam, hogy erről van szó).Az egyértelműség kedvéért még egyszer újrafogalmazom az eredeti problémát (ebből nekem is tovább tisztul a kép). Tehát adott egy absztrakt osztályhierarchia (és hozzá interfészek), melyet osztályhierarchiák valósítanak meg. Ha egyes osztályok más osztályokat is várnak paraméterül, akkor a felületen ezekhez a metódusokhoz csak az absztrakt típust adhatom meg, és ez nem szép. A kérdés, hogy hogyan oldható fel illetve előzhető meg ez a kavarodás.
Minden rétegnél kellene
Egyébként jó kérdés, én is belefutottam már régebben ugyanebbe a problémába. Tudom is, hogy hol, mindjárt kikeresem, hogy megoldottam e egyáltalán... :-)
update: Már nyomokban sincs jelen a kódban, de nem emlékszem, hogy hogyan oldottam fel.
Bevallom, hogy az IoC nekem
Semmi bonyolult nincs az IoC-ben. A lényege, hogy egy adott objektum egy absztrakt típusnak (tipikusan interface) a környezete által meghatározott konkrét implementációját fogja használni, nem ő mondja meg, hogy melyikkel dolgozik. Másképp megfogalmazva az osztály és függősége között a csatolás futásidőben jön létre, nem pedig fordítási időben. Ennek vannak különböző megvalósításai, a dependency injection a legismertebb, leggyakrabban használt (és szerintem legjobb) ezek közül. Ez gyakorlatilag az, amiről itt beszélünk, illetve amit inf3rno írt: az objektumnak kívülről besetelik a függőségeit valamilyen módon. Ettől az osztály hordozhatóvá válik. Ez ugye megköveteli az absztrakció szerinti függőséget (SOLID). Vagy megvalósítja azt, attól függ honnan nézzük :)
DI konténerek
Tehát... Az adatbázis-driverek szintjén az egyes osztálycsoportok osztályai egyértelműen összetartoznak. A futásidejű függőség majd az Application vagy hasonló osztálynál jön létre, ahol a fő adatbáziskapcsolat (és hasonlók) tárolódik. Azonban az adatbázis osztályhierarchiáin belül is fontos, hogy a felület úgy legyen kialakítva (a típusokat is beleértve), hogy a "felhasználói" kódban ne kelljen (ne lehessen) függőségeket kezelni, átadni. Ezt így jól látom?
Igen, jól látod
Annyi még, hogy megerősíteném inf3rno-t: alapvetően interface-re tervezz, ne abstract class-ra.
Köszönöm
$statement->execute()
futtatja a queryt, és a $statement belsejében csücsül a hozzá tartozóConnection
objektum. (Ebből is látszik, hogy a rossz példák mindig több sebtől vérzenek, így nem egyértelmű, hogy ezek közül melyiket akartuk prezentálni.)Majd igyekszem az interfészeket előretolni az absztrakt osztályokkal szemben. Ez persze a névadási konvenciók módosításával fog járni. Így mondjuk az interfész neve Connection lesz, az absztrakt osztályé AbstractConnection, az implementációé pedig pl. MysqlConnection.
Köszönöm a válaszokat! Úgy hiszem, hogy sikerült helyre tennem magamban az ügyet. Még adok egy hetet a "tudatalattimnak", hogy érlelje a dolgot, aztán jövőhétvégén átdolgozom az adatbáziskezelőt.
Majd igyekszem az
Igen, pontosan így kell.
Kéne a Csomag-ra is egy
Egyébként ha interface-t használnál, akkor jobban látható lenne, hogy hol a gubanc, és egyáltalán nem mindegy, hogy absztrakt osztály, vagy interface. Az absztrakt osztályok fölé mindig tegyél interface-t.
Felület
kinyit()
metódussal aCsomag
átveszi a feldolgozás felelősségét aFeldolgozo
osztálytól. A probléma az volt, hogy speciális csomaghoz speciális címzett tartozik, ahol az elvégzett művelet a címzett fő felelőssége, és a speciálisság meg is jelenik a felületben. De látom, hogy az egészet úgy kell felépíteni, hogy utóbbi fel se merüljön. Röviden: a felület nem lehet absztrakt abból a szempontból, hogy paraméteresen határozzon meg egy protokollt; egyértelműnek kell lennie, másként nincs is értelme. A visitor pattern az alaphibát nem oldja meg automatikusan, meghát az eredeti adatbázisos problémámnál nem is életszerű.Szerk
Igen, úgy terveztem, hogy a végén, ha véglegesültek az API-k, összeállítom az interfészeket is. Ezt igazából egy mellékprobléma is késlelteti (classloader vs. interface-ek).
(Közben a másik szálon is válaszoltam.)
Ja, szoktam szerkesztgetni
Mi is a hiba itt...
Alapjában az a gond, hogy ez a nyitóposztban bemutatott dolog nem öröklés, hanem típusparaméterezés. Vagyis valójában generikus típust kellene használni, ami viszont nem része a PHP nyelvnek.
A PHP alapkönyvtárában sok olyan osztályt találunk, amelyek klasszikusan generikusak, a probléma ezek miatt még jelentősebb. A dokumentáció megkerüli az egészet azzal, hogy nem foglalkozik típusokkal (lsd. pl. ArrayAccess dokumentációja), hallgatólagosan jelezve, hogy "ne adj meg típust!". Mondhatjuk persze, hogy ez bizonyos értelemben összhangban van azzal, hogy pl. a visszatérési típus fogalma nem is létezik a PHP esetében. (Úgy tűnik, a PHP még 2014-ben sem alkalmas arra, hogy valódi OOP-nyelvvé váljon.)
Az 5.3-5.5 verziók között jelentős inkompatibilis változások történtek, érzékenyen érintve a jelenleg tárgyalt kérdést is. Az alábbi kód PHP 5.3-ban nem ad hibát, valamint az elvártnak megfelelően működik:
5.5-ben pedig fatális hibával végződik a kísérlet.
Megjegyzem, nem ismerek más példát rá, hogy (hasonlóan széleskörben használt projekt esetében) azonos főverzión belül ilyen könnyedén bánjanak az inkompatibilis változtatásokkal.
PHP 5.3-ban tehát még lehetőség volt a generikus típusok (mindenféle deprecated/strict hiba nélküli) emulálására, 5.5-ben pedig ez már fatális hiba. Mikor a nyitó posztot beküldtem, még 5.3-ban dolgoztam, ezért az akkor példáim 5.5-ben már le sem futnak.
Végülis arra a megállapításra jutottam, hogy PHP esetében a típusok nyelvi szintű megadása nem igazán fontos. A legtöbb típusadatot megadhatjuk a phpdoc-ban is, amit kódolási időben vissza is kapunk minden komolyabb szerkesztőben. Ehhez persze jó volna, ha lenne egy jól használható @generic tag.
Továbbá használhatunk/készíthetünk PHP-alapú metanyelvet, ami publikáció előtt (vagy akár az IDE-n belül integrálva azonnal) jelzi a típusos hibákat. Ha a metanyelv teljesen átveszi a típusok kezelését, könnyen megvalósíthatók generikus típusok is, vagy bármilyen más elképzelés (ráadásul akár a lefutási idő/költség is rövidülhet, mert nincs nincs típusellenőrzés PHP-részről).
Egy másik megoldási irány a felhasználói típusellenőrzés valamilyen megkönnyítése. Erre vannak szép megoldások, de azért a type hinting egyszerűségétől mindegyik messze van.
Az új kérdés tehát: mi a legjobb (elegáns) megoldás a generikus típusok emulálására PHP nyelven?
A példád sérti az LSP-t, ne
A kliens egy EntityHandler típusú objektum handle() metódusának joggal ad át egy tetszőleges Entity objektumot. Ha futásidőben az EntityHandler valójában PersonHandler, akkor a metódushívásnál elszáll az egész.
LSP
(A PHP még mindig nagyon hiányos OOP-szempontból. Miért éppen itt olyan nagy divat a tiszta OOP erőltetése, miközben gyakorlatilag minden komolyabb OOP-nyelvben vannak ellensúlyozó, egyszerűsítő mechanizmusok (pl. a generikusság/template-ek)?)
Egyébként...
Type hint
Cél...
tehát...
ad 2) Velejétől hibás az ilyen megoldás, vagy "csak" az OO-t (LSP) szegi meg?
ad 3) A PHP nyelvben az 5.3-5.5 verziószakaszban eltűnt a generikus típusok használatának lehetősége.
ad 4) Hogyan emulálhatunk ezután generikusságot? Szükség van-e generikus típusokra egy PHP-programban? Miért nem vették figyelembe a nyelv tervezői (eddig is csak véletlenül működött)? Van esélye, hogy később nyelvi szinten megvalósítják?
Én értem a problémádat, de
PersonHandler implements EntityHandler
).PersonHandler implements EntityHandler
Egyetértek
LSP vs interface vs generics
Tehát valójában az 5.5-ben javítottak a php-n, bár ismét bevittek egy mélyütést a programozó bázisuknak az inkompatibilitással. Nem erősségük a koncepció, az biztos...
5.5
Egyébként hogyan kellene létrehozni PHP nyelven pl. egy ArrayList-et, ami csak Person-okat tárolhat? Mi a legjobb megoldás erre? És az örököltetésre generikus típusból? Van erre esetleg valami általános design pattern, ami tiszta OOP?
Erőlködhetsz, de nincs
precore
Max injektálhatsz closure-t,