2019. április 25., csütörtök

Gyorskeresés

PHP dependency injection container

Írta: | Kulcsszavak: php . dependency injection . container . reflection

[ ÚJ BEJEGYZÉS ]

A dependency injection (függőségbefecskedzés, DI) lehetőséget ad arra, hogy megválasszuk és egyértelműsítsük, hogyan és miből akarjuk létrehozni alkalmazásaink egyes részeit. Kicsit hülyén hangzik, de erről van szó.

Fabian Potencier példájánál maradva (nagyvonalakban), ha van egy User osztályunk, aminek szüksége van valamilyen tárolóra, amiben ténylegesen lesznek a User property-ei, akkor azt ne a User ctor-ában példányosítsuk, hanem adjunk lehetőséget arra, hogy mindenki megválassza a saját módszerét.

Első:
class User {

protected $_storage;

public function __construct() {
$this->_storage= new SessionStorage();
}
}

Második:
class User {

protected $_storage;

public function __construct(IStorage $storage) {
$this->_storage= $storage;
}
}

Ugye az elsőt sokkal könnyebb használni (biztosan így van ez? :N ), ellenben lehetetlen testreszabni, hogy nekem konkrétan milyen tárolóm is van, itt csak egyfélére vagyok korlátozva. A második esetében a kód mindenképp hosszabb lesz, hiszen meg kell írnunk egy interface-t, majd ezt implementálva tudunk csak különféle storage megoldásokat szállítani a User-nek példányosításkor. Mindenki láthatja, hogy így gyakorlatilag akármilyen tárolási mechanizmussal előállhatunk, könnyen tudjuk implementálni a változást, de könnyebben is tudjuk használni az egészet, ha ez nem is egyértelmű annyira. Az első esetben fogalmunk sem lesz arról, hogy mi fog történni akkor, mikor egy User létrejön a rendszerünkben, ha ez egy kész library lenne, akkor hányan néznének bele a kódba, hogy ugyan, mi is van benne? (Főleg a kevésbé gyakorlottak, akiknek csak működő megoldás kell?)

A DI container-ek pont könnyű használat és a testre szabhatóság közötti egyensúlyt hivatottak megtartani, remélhetőleg több sikerrel, mint Anakin-nak az Erő két oldalával. Mindebből következik, hogy tiszta kód írásához, DI alkalmazásához semmi szükség DI container-re alapból, szóval mindenki kezdje el használni most!

DI container-ek feladata a rendszer egyes elemeinek függőségeinek feloldása automatikusan a megadott logika/logikák alapján. Ez jelenthet sok mindent, én háromféle megoldást építettem bele a sajátomba:

- adott egy típus (interface, class), amire, ha valaminek szüksége van,
akkor egy másik típust (class) példányosítson a rendszer
minden egyes alkalommal
- adott típus elkéréskor egy már előre példányosított elemet adjon vissza
minden egyes alkalommal
Figyelem: ez nem Singleton! Singleton akkor lenne, ha a
rendszerben lehetetlen lenne újabb példányt létrehozni az
adott típusból, itt ez nem áll fenn!
- adott típust egy bizonyos módon oldjon fel, magát a módot egy
Closure (lambda, latinul) old fel, ennél meg lehet mondani, hogy a
container létezéséig csak egy instance létezhet a feloldó
típusból, vagy minden feloldásnál fusson le újra a lambda

Ami egy container megépítéséhez kell (PHP 5.3-on túl), az szerintem egy osztály, ami egységes felületet ad a típusok kezelésére (nálam a \System\Type), a PHP reflection API-ja és elég gyakorlat ahhoz, hogy magától beszélő kódot tudjunk írni. Sajnos a PHP reflection része jórészt dokumentálatlan, kicsit furcsán is működik:

private function _getConstructor(\System\Type $type) {
try {
if ($type->IsClass()) {
return $type->Reflection()->getConstructor();
}

return $type->Reflection()->getMethod("__construct");
} catch (\Exception $exc) {
return NULL;
}
}

Interface esetén nem képes megtalálni simán a ctor-t, mindig NULL-t ad vissza, másik csavar, hogy a getConstructor() nem dob kivételt, ha nincs, ellenben a getMethod($name) igen. Nem is PHP lenne, ha ilyenek nem lennének benne.

Amit még érdemes megoldani, az a method injection. Eddig ugyan nem emeltem ki külön, de csak a constructor injection került szóba, de miért is ne oldanánk meg a MI-t? A módszer alapjaiban véve ugyanaz, mint a __constructor() esetében: ki kell keresnünk a metódust, majd meg kell vizsgálnunk a paraméterlistáját, mindezt én a PHP 5.3-ban bemutatkozott megoldásával oldottam meg, megmondtam, hogy milyen típusú paramétereket várnak az egyes metódusok (public function Save(User $user) { /* ... */ }).

Amivel az egyes feloldások regisztrációját fel lehet tuningolni az az, hogy az egyes feloldásokat ellátjuk 1-1 névvel. Miért jó ez? Ha mondjuk a production code-unk minden regisztrációja névtelen, akkor a tesztjeinkben használhatunk olyan feloldásokat, ahol megmondjuk, hogy csak a "test" névvel ellátott feloldások érdekelnek bennünket, semmi más. Példán keresztül világosabb lesz a dolog:
Ha van egy loggoló alkalmatosságunk, amit eddig singleton-nal írtunk meg, most pedig már átváltottunk DI containerre, akkor azt a Unit test-ek során nem feltétlenül szeretnénk használni, vagy nem úgy használnánk, ahogyan a production code-unk, például élesben adatbázisba loggolunk, de a unit test-ünk során nem akarunk plusz köröket futni az adatbázis miatt (eleve, hogy legyen, kapcsolódjunk hozzá, használatba tudjuk venni), készítünk egy egyszerűsített Logger-t, ami vagy semmit sem csinál, vagy legfeljebb memóriába loggol. Singleton-t hogyan is tesztelnénk? Sehogy.

Megmutatom, milyen használni egy DI container-t:

$cont = new DIContainer();
// I interface egy A instance-szal oldjunk fel
$cont->Register(typeof("I"), typeof("A"));
// IntOfB interface-t B instance-szal, a feloldás neve "" és még az InjectE metódust is oldja fel nekünk a rendszer
$cont->Register(typeof("IntOfB"), typeof("B"), "", new MethodDependency("InjectE"));
// C típust egy lambdával oldjunk fel névtelenül úgy, hogy egy darab instance lehet ebből a feloldásból a container-en belül
$cont->RegisterLogic(typeof("C"), function () {
echo 1; return new D();
}, NULL, true);

$instanceOfIImplementation = $cont->Resolve(typeof("I"));

Az osztályokat, interface-eket nem látjátok mögötte, de mutatom, mi jön ki:

1A Object
(
[_intOfB:A:private] => B Object
(
[_data:protected] => Array
(
[0] => D Object
(
[_d:protected] => 1341083236.0576
[_typeForReflectionClass:protected] =>
)

[1] => D Object
(
[_d:protected] => 1341083236.0576
[_typeForReflectionClass:protected] =>
)

)

[_e:protected] => E Object
(
)

)

)

Ha valakinek kérdése van, tegye fel.

Hozzászólások

(#1) Athlon64+


Athlon64+
(őstag)

[ értesítő ]

(#2) Venemo


Venemo
(kvázi-tag)

Vicces, amikor oda nem illő enterprise feature-öket és koncepciókat próbálnak ráhúzni a PHP-ra. :D

(#3) Athlon64+ válasza Venemo (#2) üzenetére


Athlon64+
(őstag)

Nem szarral gurigázunk.

(#4) lezso6


lezso6
(VARÁZSLÓ)
LOGOUT blog

Én most elsőre nem látom értelmét. Másodszorra se esett le. Hülye volnék? :D

Primitív kérdésre nincs helyes válasz.

(#5) Athlon64+ válasza lezso6 (#4) üzenetére


Athlon64+
(őstag)

Biztos a meleg miatt, avagy ajjaj RI/OS. :DDD

(#6) lezso6 válasza Athlon64+ (#5) üzenetére


lezso6
(VARÁZSLÓ)
LOGOUT blog

Közben megtaláltam a wiki oldalát, és rájöttem, hogy használok (ha nem pont ilyet és így, de) hasonlót, mániám az automatizmus és a redundancia-mentesség. :D

[ Szerkesztve ]

Primitív kérdésre nincs helyes válasz.

(#7) j0k3r!


j0k3r!
(senior tag)

egy ujabb hasznos cikk :R

some men just wanna watch the world burn...

További hozzászólások megtekintése...
Copyright © 2000-2019 PROHARDVER Informatikai Kft.