ugrás a tartalomhoz

OOP - öröklött felület specializálódása

Süsü · 2013. Már. 9. (Szo), 22.02
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 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 {}
(Itt a 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!";
    }
    
}
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 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?
 
1

Liskov

BlaZe · 2013. Már. 10. (V), 00.56
A választ le is írtad, nem jó gyakorlat, pontosan a Liskov-tétel megszegése miatt, ahogy említetted is. Olyannyira nem, hogy a típusos nyelvek ez ellen védenek is, a java pl biztosan, le sem fordul az ilyesmi.

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.
2

Címzett és levél

Süsü · 2013. Már. 10. (V), 02.09
Igen értem, amit írtál, és örülök, hogy megerősítettél benne, hogy ez rossz gyakorlat.

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?

$db = Database::create(new MysqlConnection("mysql://user@pass/db"),"mysql"); // függőség gyártáskor
$db->query("select")->from("tabla")->execute()->fetch(); // nincs függőség
De ez akkor már helytelen lenne:

$db = new MysqlDatabase();
$db->init(new MysqlConnection("mysql://user@pass/db")); // függőség a felületben!
$db->run(new MysqlTransaction()); // és ez is!
3

Az első példád a jobb

BlaZe · 2013. Már. 10. (V), 11.01
Az első példád a jobb, valóban. Ezt szokták pl úgy csinálni, hogy van egy sql dialektus interface.
interface SQLDialect {
  Connection createConnection();
  Statement createStatement( String statement );
  Transaction createTransaction();
}
Stb. Illetve van hozzá egy dialect resolver, ami configban beállított érték után kiválasztja, hogy melyik dialektust kell használnod. Akár pl a connection url-ből is, ahogy a példádban benne van ez az infó. És akkor valahogy így néz ki egy példa:

  Database db = Database.create( "mysql://user@pass/db" );
  Connection conn = db.createConnection();
  Transaction trx = db.createTransaction();
  trx.begin();
  Statement stmt = db.createStatement( "..." );
  stmt.setStringParam( Foo.FIELD_ID, 13 );  
  stmt.setStringParam( Foo.FIELD_VALUE, "new value" );  
  stmt.executeUpdate();


  ResultSet resultSet = stmt.query();
  trx.commit();
  conn.close();
Itt a Database-nek lesz egy dialect példánya, és az említett metódusai erre wrapperek, vagy visszaadhatja a konkrét dialectet is, ahogy tetszik. Ezt folytatva már rá lehet építeni a query buildert is.
5

Köszönöm

Süsü · 2013. Már. 10. (V), 14.08
Nagyszerű, én is ehhez hasonlóan képzeltem. Köszönöm az eligazítást! Azt hiszem, mostmár tisztázódott, mire törekedjek a felületeknél.
6

Nem muszáj constructor

inf · 2013. Már. 10. (V), 14.10
Nem muszáj constructor injection-t használni, használhatsz setter-t is helyette, a setterek hívása után meg beteszel egy init()-et. Ilyen esetben mondjuk jobb akkor már a setterek beállítását is egy külön osztállyal végeztetni (IoC container).
8

Típusok

Süsü · 2013. Már. 10. (V), 14.56
És ha settert használok, akkor milyen típust vár a setter? Vagy ne legyen a felület része?

interface HandlerAPI {
    // ...
    public function setResource(Resource $resource);
}

class MyHandler implements HandlerAPI {
    
    // ...
    
    // ez a gond, MyHandler csak MyResource-öt kezel:
    public function setResource(MyResource $resource) {
        // ...
    }
    
}
Bevallom, hogy az IoC nekem még új. A Symfony keretrendszerben találkoztam ezzel a dologgal, de ott kicsit más értelemben szerepel, mint amire nekem most szükségem van; de lehet, hogy rosszul látom.
10

Ha jól tudom (de lehet, hogy

inf · 2013. Már. 10. (V), 16.12
Ha jól tudom (de lehet, hogy tévedek) a setter és a getter nem része az interface-nek...
11

Az IoC gyakorlatilag annyi,

inf · 2013. Már. 10. (V), 16.17
Az IoC gyakorlatilag annyi, hogy Csomagszolgáltatót adod át a Csomagfeldolgozónak, nem pedig magát a Csomagot. A Csomagszolgáltató interface-én annyi van, hogy getCsomag(), a feldolgozó meg ezen keresztül éri el a csomagot. A Csomagszolgáltató végzi a Csomag példányosítást, a tulajdonságainak a beállítását, stb... Pl megadhatsz MeglepetésCsomagszolgáltatót, ilyenkor csak MeglepetésCsomagokat kap majd tőle a feldolgozód. Mondjuk a getCsomag()-nál a visszatérő érték is Csomag interface-t kell, hogy implementáljon. Elméletileg java-ban meg a többi erősen típusos nyelvben meg lehet csinálni ezek után, hogy a feldolgozó osztályodnál valahogy átkasztolod MeglepetésCsomagra, de ehhez én nem igazán értek, hogy ott mi a szokás...
13

Mégegyszer

Süsü · 2013. Már. 10. (V), 16.45
Világos, bár az eredeti probléma szempontjából megintcsak mindegy, hogy MeglepetésCsomag vagy Meglepeté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.
14

Minden rétegnél kellene

inf · 2013. Már. 10. (V), 16.57
Minden rétegnél kellene használni IoC-ot, mert az osztályok példányosítása egy külön feladat, tehát az az osztály, ami más osztályokat példányosít meg még azon kívül mást is csinál sértené az SRP-t. Egyébként ezzel nem nagyon szoktak foglalkozni, csak így lenne helyes.

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.
15

Bevallom, hogy az IoC nekem

BlaZe · 2013. Már. 10. (V), 18.58
Bevallom, hogy az IoC nekem még új. A Symfony keretrendszerben találkoztam ezzel a dologgal, de ott kicsit más értelemben szerepel, mint amire nekem most szükségem van; de lehet, hogy rosszul látom.

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 :)
16

DI konténerek

Süsü · 2013. Már. 10. (V), 21.05
Tulajdonképpen már használatam DI konténereket, a lényeget értem; sőt, kifejezetten tisztelettel tekintek ennek a megoldásnak a szépségére (különösen az egyéb "megoldásokra" gondolva). Ennek ellenére ez még elég új a számomra, főleg ami a részleteket és a gyakorlatot illeti.

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?
17

Igen, jól látod

BlaZe · 2013. Már. 10. (V), 21.07
Igen, jól látod. Egyszer kelljen neki átadnod egy driver/dialektus típust, és onnantól a kódodat ne érdekelje, hogy milyen driver dolgozik alatta (nyilván az sql szerverek között vannak különbségek, amikre figyelni kell, de ez ebből a threadból kivezet, úgyhogy hagyjuk is).

Annyi még, hogy megerősíteném inf3rno-t: alapvetően interface-re tervezz, ne abstract class-ra.
18

Köszönöm

Süsü · 2013. Már. 10. (V), 21.39
Értem. Természetesen az egész adatbázis-konténeresdi lényege, hogy a felhasználói kódra nem tartozik, hogy milyen driver megy alatta. Amit most tisztáztunk, az tulajdonképpen az, hogy "rejtett" módon se tartozzon rá, pl.:

$conn = Database::create("mysql(mysql)://user@pass/db");
$statement = $conn->query("select")->from("table");

// es akkor: (nem mintha ez életszerű lenne, de hirtelen ez jutott eszembe)
$conn->runStatement($statement);

$conn->runStatement($olderStatement); // FATAL!
// Hoppá, ez egy oracle statement volt, csak erről a nagy kavarodásban elfelejtkeztünk
Természetesen a valóságban a $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.
19

Majd igyekszem az

inf · 2013. Már. 11. (H), 04.25
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.


Igen, pontosan így kell.
4

Kéne a Csomag-ra is egy

inf · 2013. Már. 10. (V), 14.06
Kéne a Csomag-ra is egy metódus, mondjuk kinyit() és akkor elkerülhető ez a hiba. A másik megoldás, hogyha a feldolgozó nem abstract és a visitor mintát használod.

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.
7

Felület

Süsü · 2013. Már. 10. (V), 14.41
Köszönöm az észrevételt; bár a kérdésem a felületöröklésre vonatkozott, az osztályok egymás közötti interakciója már más kérdés. A kinyit() metódussal a Csomag átveszi a feldolgozás felelősségét a Feldolgozo 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ű.
9

Szerk

Süsü · 2013. Már. 10. (V), 15.04
Azt hiszem, időközben szerkesztetted a hozzászólásodat.

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.)
12

Ja, szoktam szerkesztgetni

inf · 2013. Már. 10. (V), 16.18
Ja, szoktam szerkesztgetni elég sűrűn... :-)
20

Mi is a hiba itt...

Süsü · 2014. Ápr. 30. (Sze), 17.59
Igaz, hogy a topic már több mint egy éves, de gondoltam, a jövőbeli látogatók kedvéért jobb ha leírom az - utólag már viszonylag banálisnak tűnő - megállapításaimat.

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:

<?php

error_reporting(E_ALL);
ini_set("display_errors","1");

interface Entity {
    public function getName();
}

class Person implements Entity {
    
    private $name;
    
    public function __construct($name) {
        $this->name = $name;
    }
    
    public function getName() {
        return $this->name;
    }
    
}

/* generic */ interface EntityHandler {
    
    function handle(Entity $oEntity);
}

class PersonHandler implements EntityHandler {
    
    public function handle(Person $oPerson) {
        var_dump($oPerson);
    }
    
}

?>
PHP 5.4-ben már kapunk egy E_STRICT hibát.

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?
21

A példád sérti az LSP-t, ne

szjanihu · 2014. Ápr. 30. (Sze), 19.47
A példád sérti az LSP-t, ne csinálj ilyet.

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.

public function clientMethod(EntityHandler $handler) {
  $handler->handle(new Animal());
}
23

LSP

Süsü · 2014. Ápr. 30. (Sze), 21.08
Természetesen sérti az LSP-t. Kérlek, olvasd el a teljes hozzászólást! Hogyan implementálnál alapvetően generikus struktúrát? Vagy akkor ilyesmit ne is tervezzünk PHP nyelvre?

(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)?)

class FancyDoubleList extends ArrayList<Double> {
    public boolean add(Double x) {
        return parent.add(x+42);
    }
}
28

Egyébként...

Süsü · 2014. Ápr. 30. (Sze), 21.47
Egyébként a példád jó, mert bizonyos értelemben a lényegre tapint. Az EntityHandler (mint paraméter) egy nem teljes típusmegjelölés. Javában az ilyesmi warninggal jár, tehát van visszajelzés arról, hogy valami nincs rendben.
22

Type hint

Hidvégi Gábor · 2014. Ápr. 30. (Sze), 21.05
Mi a célod a típus megjelelölésével? Nem tudod, hogy mi(lyen objektumo)t adsz át paraméterként, és mondjuk el szeretnéd kapni a kivételt?
24

Cél...

Süsü · 2014. Ápr. 30. (Sze), 21.15
Nem cél a kivétel elkapása. Mi a cél bármilyen típusmegjelöléssel? Dokumentáció, autocompleting, átláthatóság stb.
25

tehát...

Süsü · 2014. Ápr. 30. (Sze), 21.28
ad 1) A nyitóposztban vázolt probléma abból fakad, hogy generikusan tervezett mintát akart használni OO-környezetben.

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?
26

Én értem a problémádat, de

Hidvégi Gábor · 2014. Ápr. 30. (Sze), 21.32
Én értem a problémádat, de nem tudok rá válaszolni, mert máshogy programozok. Viszont érzésem szerint az LSP-t nem szegi meg (mivel PersonHandler implements EntityHandler).
27

PersonHandler implements EntityHandler

Süsü · 2014. Ápr. 30. (Sze), 21.46
Igen, de valójában:
PersonHandler implements EntityHandler<Person>
Megjegyzem, én nagy híve vagyok a mély OO-nak, de érzésem szerint nem egészséges egyes dolgokat OO-szintre vinni. Bizonyos nem-OO elemek jól kiegészíthetik az OO-programokat, ezek közül szerintem első helyen áll a generikusság. Jelen esetben szerintem nem baj, ha nincs EntityHandler OO-ősosztály, mert úgysem használjuk sehol. Van PersonHandler, AnimalHandler és ProgrammistHandler, de mindegyik önálló őstípus. Külön kérdés, hogy van egy template-jük EntityHandler néven, mintegy metastruktúraként az OO-réteg fölött.
29

Egyetértek

Hidvégi Gábor · 2014. Ápr. 30. (Sze), 22.00
Igen, szerintem is így lenne logikus. Egyébként szerintem az, hogy 5.3 alatt működött a dolog, inkább a véletlen műve.
30

LSP vs interface vs generics

BlaZe · 2014. Ápr. 30. (Sze), 22.21
Azért szegi meg, mert az interface által leírt contractban azt ígéred, hogy minden EntityHandler típust támogatni fogsz minden implementációjában. De ezt a contractot megszeged, mivel az adott implementációban csak a PersonHandlert támogatod. Genericsnél ez máshogy néz ki, ott gyakorlatilag egy lokális tipizálásáról van szó egy általános contractnak, vagyis az adott osztály felhasználója maga definiálja, hogy milyen típusra kéri a contractban garantáltakat. Emiatt ott ebben a kontextusban nem játszik a LSP megsértése. Feltéve, hogy a felhasználó definiálja a típust. Ha nem, akkor természetesen lehet belőle probléma. Ezért is warningozik a java compiler is a raw typera.

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...
31

5.5

Süsü · 2014. Ápr. 30. (Sze), 22.58
Igen, én is úgy értelmezem, hogy ez egy javítás volt, azért is írtam, hogy "véletlenül" működött (annál is inkább, mert valójában pármilyen paraméter-módosítás megengedett volt). Az EntityHandler (nyelvileg) normál definíciója valóban problémás, mivel nemkívánatos contractot hozok létre (de ha egyszer nincs más mód generikusságra - kivéve ha egyáltalán nem adok meg típusokat, ami még rosszabb).

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?
33

Erőlködhetsz, de nincs

szjanihu · 2014. Május. 1. (Cs), 10.47
Erőlködhetsz, de nincs generics PHP-ben. Dokumentáld az API-ban, hogy minek kell ott lennie és/vagy ellenőrizd a típust és ha nem az elvárásoknak megfelelő, dobj kivételt. Ez sajnos ilyen PHP-ben.

precore

ObjectClass::forName(Person::class)->cast($entity);
Ettől ez még nyilván sérti az LSP-t ennél a konkrét példánál.
32

Max injektálhatsz closure-t,

inf · 2014. Május. 1. (Cs), 00.29
Max injektálhatsz closure-t, ami ellenőrzi a típust, sokat nem tehetsz, gagyi a nyelv... Koncepció helyett meg csak dobálódznak fél megoldásokkal...