ugrás a tartalomhoz

Dependency Injection

szabo.b.gabor · 2010. Nov. 17. (Sze), 10.19

Foglalkoztat mostanában a kérdés, hogy hogyan hagyhatnám el a globális változók használatát. Bár engem nem zavar, de azért érzem, hogy gáz, meg egyéb okok is úgy döntöttem, hogy váltani kell. Innen indult a dolog, kutakodtam, keresgéltem, nézegettem többfelé, végül találtam egész érthető anyagot. Eredményeim megosztanám, várom az építő jellegű kritikákat, hozzászólásokat, megerősítéseket, pofonokat.

Én a programjaim futásának elején példányosítottam a szükséges osztályokat, majd ezeket adott helyen global kulcsszóval elértem.

$_DB=new DB();
# ...
# ...
class Ize{
	function bigyo(){
		global $_DB;

		$_DB->query('SELECT kisfroccs FROM kocsma');
	}
}

Értem én, hogy nem tudni mi van benne, meg bele lehet nyúlkálni. Engem a $_GET változó sem zavar, utána lehet nézni, valamint nincs az a biztonságos framework, ami ne lenne csodákra képes egy elég hülye programozó kezében. Menjünk tovább.

Osztályok singletonná alakítását hamar elvetettem. Felesleges bonyolítás, nem használhatok több példányt ha éppen szeretnék, ami elég nagy érvágás. A Singleton Multiton megoldás már tetszett, bár kissé feleslegesnek tartom a ketté választást. De az ötlet, hogy egy nyilvántartó osztályt használjunk megtetszett. Tehát igazából itt már el is érhettem volna a célomat, globális változóimat lecserélhettem volna ily' módon is, metódusaimból némi plusz gépelés árán eltűnhettek volna a global kulcsszavak és mindenki boldog. De feltűnt a láthatáron az egységtesztelés és a dependency injection (DI), úgyhogy muszáj volt tovább menni.

A DI lényege – ahogy sikerült megértenem –, hogy megmondom, mi kell nekem, a framework pedig alámrakja. Nem kertelek tovább, a megoldásom undefined property-n és __get() függvényen alapul. Van egy ősosztály.

class DI{
	function __get($name){
		/* so we'll have some cases when we'd like to return an object
		 * returned by so called InstanceManager
		 * */
		if($name[0]!='_'){ return null; }
		$instance=InstanceManager::get(substr($name,1));
		$this->$name=$instance;
		return $instance;
	}
}

Ha valamelyik osztályban szeretnénk DI-t használni, akkor a DI-ből kell származtatnunk.

Ezen kívül van egy InstanceManager.

class InstanceManager{
	private static $instances=array();
	private static $rules=array();
	
	public static function get($instanceID){
		if(!empty(self::$instances[$instanceID])){
			return self::$instances[$instanceID];
		}
		
		#not found we have to create the instance first
		if(isset(self::$rules[$instanceID])){
			$r=self::$rules[$instanceID];
			$class=$r[0];
			$args=$r[1];
			#TODO: have to handle calling a constructor with multiple arguments
			$instance=new $class($args); #for an object waiting for two params in construct this would fail..
			self::$instances[$instanceID]=$instance;
			return $instance;
		}else{
			return null;
		}
	}
	
	/**
	 * through this function we can initialize rules to map $instanceIDs
	 * to classes and arguments
	 * passed array should look like this
	 * array(
	 * 	$instanceID=>array($classname,$arguments)
	 * )
	 * */
	public static function setRules($array){
		if(empty(self::$rules)){
			self::$rules=$array;
		}else{
			throw new Exception('rules already loaded this is probably an error..');
		}
	}
}

Nem teljes, de az elgondolás talán látszik.

Teszteléshez példa pediglen így néz ki.


# ize
# bigyo
# DI, InstanceManager itt mar elerheto

InstanceManager::setRules(array('PATH'=>array('Path',null)));

class Path {

	protected $root;
	protected $www;
	protected $classes;
	protected $templates;
	
	function __construct() {
		$DS=DIRECTORY_SEPARATOR;
		$this->root=str_replace($DS . "classes".$DS."waf","",dirname(__FILE__)).$DS;
		$this->www=$this->root."www" . $DS;
		$this->classes=$this->root."classes" . $DS;
		$this->templates=$this->root."templates" . $DS;
	}
	
	function __get($name){
		if(property_exists($this,$name))	return $this->$name;
		else return false;
	}
}

class Jani extends DI{
	function test(){
		echo $this->_PATH->root,"\n";
	}
}

$j=new Jani();
$j->test();
$j->test();

Lássuk, mi történik itt. Janiban a test() metódusban hivatkozott $this->_PATH nincs definiálva, ezért meghívódik a DI::__get() függvénye, ami felismeri, hogy neki itt cselekednie kell, meghívja az InstanceManager::get() metódusát. Észreveszi, hogy a PATH indexű móka még nincs példányosítva, megteszi ezt, tárolja és visszaadja, és DI mindezt Janiban még el is tárolja. Végre lefuthat az első test() metódusa Janinak. A második test() hívásnál már a DI::__get()-ig sem jutunk el, hiszen itt már rendelkezésre áll az osztályváltozó.

Biztosan vannak a megvalósításban hibák, nem tudom, hogy mennyire tesztbarát a megoldás, vagy sem (a unit test egy következő lépés számomra), de nekem tetszik az egyszerűsége. Remélem nem nyúltam nagyon mellé.

 
1

Mi a DI osztály?

salla · 2010. Nov. 17. (Sze), 19.31
És gyakorlatilag mit csinál a DI osztály ezen kívül? Nem igazán látom át, hogy miért ne lehetne összevonni az InstanceManager-rel.
3

a DI osztály ezenkívül nem

szabo.b.gabor · 2010. Nov. 17. (Sze), 20.10
a DI osztály ezenkívül nem csinál semmit, igazából nincs is rá szükség, elég ha azokban az osztályokban ahol Dependency Injection-t szeretnénk használni megvalósítjuk a __get függvényben található logikát.. így hogy osztályban van és származtatsz belőle, könnyebben kezelhető talán. nem biztos. mindenesetre így szebbnek találtam.

összevonni az InstanceManager-rel nem volna nyerő. a DI a példányosított osztályokkal van kapcsolatban, az InstanceManager pedig ezeket kezeli. a kettő nem ugyanaz..
2

Ez így nem nyerő

inf3rno · 2010. Nov. 17. (Sze), 19.44
Ez így nem nyerő megközelítése a problémának, nem jó, hogy DI-ből származtatsz, inkább külső osztályok kellenek, amik felépítik az objektum fát. Nézz után a builder pattern-nek meg a multiton-nak. Hasonlítsd össze amit írtál egy multitonnal, aztán talán meglátod mire gondolok. Szinte ugyanaz a kettő.
Dependency injection-nek egyébként egyáltalán nem az a lényege amit itt csináltál, arra való, hogy a laza csatolást fenntartsuk az objektumok között, te meg inkább a létrehozó mintákra jellemző dolgokat helyezted előtérbe, di-nek viszont nem ez a lényege.
4

Laza csatolás az

szabo.b.gabor · 2010. Nov. 17. (Sze), 20.15
Laza csatolás az InstanceManager::setRules metódusa révén valósítható meg, azt mondom egy osztályban, hogy $this->_DB, de hogy a DB végül milyen osztályban ölt testet az a configon múlik.
6

Ez nem laza csatolás, amit

inf3rno · 2010. Nov. 17. (Sze), 23.35
Ez nem laza csatolás, amit írsz. A laza csatolás az, amikor kérsz egy objektumot, aminek olyan a felülete, amilyenre szükséged van. Arról semmit sem kell/szabad tudni, hogy milyen a belső szerkezete, meg hogy milyen az osztálya.

Javaslom, hogy olvasd el ezt:
http://components.symfony-project.org/dependency-injection/trunk/book/01-Dependency-Injection

(Hehe, tőlem pont egy olyan blogbejegyzést linkeltél be, amire nem vagyok túl büszke. Ugyanazt a hibát követtem el, mint te most. Úgy látszik visszaüt rám. :-) )
7

szerintem amit csinálok az

szabo.b.gabor · 2010. Nov. 18. (Cs), 00.14
szerintem amit csinálok az Property Injection kicsit speciális, de kényelmes módon. de azt azért mond el nekem a példánál maradva, hogy Jani hol tud a '$this->_PATH' osztályáról, belső szerkezetéről bármit is? oké azt tudja, hogy a _PATH tartalmazni fog egy root változót, amiben a rendszer gyökerének az elérése lesz megtalálható. vagy mondjuk azt tudja, hogy a '$this->_DB'-ben lesz egy adatbázis kezelő valami, aminek lesz query meg fetch függvénye, de mást nem.

nem látom, hogy az ellenérveid hol érvényesek a bemutatott megoldásra? valószínűleg az kavar be, hogy az InstanceManager úgymond singletonokat ad vissza, vagy nem tudom, de akár adhatna vissza mindig új példányt is, ha arról van szó. ez már konfigurálás és megvalósítás kérdése.

ha meg az általad linkelt oldal symfony-s vagy zend-es példáit nézzük oda nemigen kell varázslat, megcsinálható bármikor.

http://www.theserverside.com/news/1321158/A-beginners-guide-to-Dependency-Injection

sztem ez jobban leírja a dolgot..

(amúgy az említett link az egy bejegyzésednek egy hozzászólása volt, és az alatta lévő dolgok :))
5

Nos, lehet hogy tévedek, de

neogee · 2010. Nov. 17. (Sze), 22.53
Nos, lehet hogy tévedek, de szerintem a DI-hez nem feltétlenül kell semmiféle osztályt használni. Mármint úgy értem, hogy a DI alapvetően azt valósítja meg, hogy elhagyhassuk az osztályokon belüli new kulcsszavakat, és ehelyett kivülről adjunk át az adott objektumnak egy másik objektumot, (lehetőleg olyat amilyet vár...) ezzel csökkentve az objektumok közötti függőségeket :) Bár lehet hogy ez nagyon le van egyszerűsítve, de ebben az értelmezésben abszolút elegendő egy registry illetve a factory minta alkalmazása a megfelelő helyeken. :)

Én speciel úgy szoktam megvalósítani, hogy van egy 'core' objektum, ami tartalmazza a program alapvető futásához szükséges objektumokat, és ezt a core objektumot adom át a controllereknek illetve a moduloknak, és minden olyan objektumnak, aminek szüksége van a szolgáltatásaira :) Tulajdonképpen ez a core ez csak egy tároló, amiben a várhatóan sokszor használt objektumok vannak. Sql kapcsolatok esetén pl a factory minta segítségével oldom meg ezt a tárolást, és a létrehozott sql kapcsolati osztály bekerül egy SqlRegistry-be, ahonnan aztán bármikor elérhető egy kulcs segítségével. Persze a factory konfigurálható, hogy keressen e a már létezők között és ha talál akkor azt adja e vissza, vagy pedig mindenképpen új kapcsolatot szeretnénk létrehozni :)

Nem tudom, hogy ez így mennyire helyes megoldás, de nekem eddig abszolut működött. :)

Egyébként, mi van akkor, ha én a származtatott osztályban szeretnék egyedi __get() metódust definiálni? :)
Ezt csak azért kérdezem, mert az ügye felülírja a DI osztály _get() metódusát, és így borul minden. :)

Ami nekem ezzel kapcsolatban kérdése, az az, hogy tegyük fel van egy osztályom amiben szeretnék könyvtárakkal történő munkát végezni. Például egy könyvtárstrukturát létrehozni. Erre készítek egy DirectoryManager osztályt. Eddig nincs is gond. A kérdés az, hogy hogyan kerülhetem el, hogy az osztály tagfügvényében ki keljen adnom a $dm = new DirectoryManager(); parancsot? Ezt csak nem tölthetem be előre, hiszen ez csak hébe-hóba fog lefutni, ráadásul csak az adott osztály bizonyos metódusaiban, és nem az összesben. Erre milyen technikák vannak? :)
8

Egyébként, mi van akkor, ha

szabo.b.gabor · 2010. Nov. 18. (Cs), 00.18
Egyébként, mi van akkor, ha én a származtatott osztályban szeretnék egyedi __get() metódust definiálni? :) Ezt csak azért kérdezem, mert az ügye felülírja a DI osztály __get() metódusát, és így borul minden. :)


parent::__get() ?
9

neogee +1 Annyival

vikos · 2010. Nov. 18. (Cs), 16.35
neogee +1

Annyival kiegészíteném, hogy az igazán nagy előnye hogy az osztályod /objektumaid csak a neki szükséges objektumok/osztályok felületétől függenek. Bármikor lecserélhetőek a belül kezelt objektumok.

Ahogy a Potencier-féle példában is volt... Ha kitalálod hogy APC-ben tárolod a session-t akkor egy APC elérést nyújtó objektumot adsz a session kezelődnek ha DB-ben akkor annak megfelelőt.

Ha belül hozod létre akkor nem tudod variálni.
10

law of demeter

Hodicska Gergely · 2010. Nov. 19. (P), 21.46
Ez a core objektum kapásból nem jó, mert ezzel ugyanúgy egy globális változót vezettél be, mintha globals-t használnál, illetve tesztelhetőség/tervezés szempontjából sem szerencsés: nézd meg, hogy mi az a law of demeter, és egyből látni fogod, hogy miért.

Szvsz a cikkíró megközelítése sem az igazi, már az nem jó ötelt, hogy a DI-ből kéne szármznia az osztályoknak.

Mindkettőtöknek ajánlom, hogy induljatok mondjuk el ebből a blogbejegyzésből: http://misko.hevery.com/2008/08/17/singletons-are-pathological-liars, és szépen mindenhol kövessétek a linkeket a további posztjaira. Ebből nagyon jó képet fogtok kapni, és meg lehet érteni, hogy miről is szólna ez az egész.
11

Hmm elolvasom mindenképpen.

neogee · 2010. Nov. 24. (Sze), 07.30
Hmm elolvasom mindenképpen. Köszi szépen. :)
12

Elolvastam amit linkeltél, és

szabo.b.gabor · 2010. Nov. 25. (Cs), 09.22
Elolvastam amit linkeltél, és a follow up cikkeket is, nagyon hasznosak. jó arc ez a misko, és még magyarázni is tud.

amit leszűrtem. ha mások által is értelmezhető kódot akarok írni, akkor minden osztályban egyértelművé kell tenni, hogy milyen más osztályokkal működik együtt, hogy akármilyen metódusba ha belenéz, akkor el tudjon jutni kód alapján az összes együttműködő elemhez. 'code bubbling' :) valamint másik fontos dolog a tesztelhetőség szempontjából, hogy az együttműködő osztályokat ne az osztály maga hozza létre, hanem kérje őket..

Now, in order to have a testable codebase we have to make sure that we don’t mix the object construction with application logic. So all of the above objects should rarely call the new operator (value objects are OK). Instead each of the objects above would declare its collaborators in the constructor.

In our tests it is now easy to instantiate the graph of objects and substitute test-doubles for our collaborators.


a where have all the singletons gone cikkében azt írja, hogy legyen egy factory objektumunk ami létrehozza a szükséges objektumokat és ezt a factory-t adjuk át az alkalmazás szereplőinek. majd persze innen oda lyukad ki, hogy a factory-nkat persze nem is kell megírnunk, hiszen vannak erre keretrendszerek.

Ilyen keretrendszer php-ban is van (ahogy a bevezetőmben található link is mutatja). nem tudom, hogy java-ban egy ilyen keretrendszer overhead-je hogyan jelentkezik, de én úgy gondoltam, hogy egy framework (haha, ez most akkor szóismétlés?) használata helyett, alkalmazok néhány egyszerű szabályt, amik bár felrúgják a kód 'tisztaságát', de az elvárt működést biztosítják és remélhetőleg kisebb plusz terhelést raknak a rendszerre, mint egy framework.

az alkalmazott szabály pedig annyi, hogy minden osztályváltozó ami '_' alulvonással kezdődik és csupa nagybetű, egy egy InstanceManager (misko megközelítésben Factory) által létrehozott osztályra való hivatkozás. valamint van még egy megkötés az pedig annyi volna, hogy az instancemanager setrules-t meg kell hívni.

nem biztos, hogy jól értem a tesztelés, mockolás témakört (mert még nem alkalmaztam), de azt hiszem, hogy az un mockolás egy megfelelő setrules hívással megvalósítható.

neogee kérdésére a válasz a DirectoryManagerrel kapcsolatban. ha jól sejtem akkor erről a módról

class Ize{

protected $dm;

function __construct($core){
	$this->dm=$core->getDm();
}

function fv1(){/*nem hasznal dm-et*/}
function fv2(){/*nem hasznal dm-et*/}
function fv3(){
	$this->dm->createDir();
}

}

$ize=new Ize($core);
$ize->fv3();
kellene áttérned ilyemire

class Ize{

function fv1(){/*nem hasznal dm-et*/}
function fv2(){/*nem hasznal dm-et*/}
function fv3($dm){
	$dm->createDir();
}

}

$ize=new Ize();
$ize->fv3($core->getDm());