ugrás a tartalomhoz

Szerializálás: külső referenciák injektálása

inf · 2011. Nov. 18. (P), 02.22
Szeretnék részleteket cachelni az aktuális állapotáról a rendszernek úgy, hogy a visszaállításkor az adott részlet automatikusan visszaépüljön a visszaállításkori rendszerbe. Így egy sokkal bonyolultabb cachelési logika valósítható meg... Gondoltam megosztom, hogy mire jutottam:

class Identity
{
	static protected $instance;
	static public function Instance()
	{
		if (!isset(self::$instance))
			self::$instance=new self();
		return self::$instance;
	}
	
	protected $map=array();
	
	public function hashCode($object)
	{
		$hashCode=spl_object_hash($object);
		$this->map[$hashCode]=$object;
		return $hashCode;
	}
	
	public function objectFromHashCode($hashCode)
	{
		return $this->map[$hashCode];
	}
}

class State
{
	public $value;
	
	public function __construct()
	{
		$this->value=rand(0,111);
	}
}

class MyClass
{
	public $state;
	public $stateHash;
	
	public function __construct()
	{
		$this->changeState();
	}
	
	public function changeState()
	{
		$this->state=new State();
	}
	
	public function __sleep()
	{
		$this->stateHash=Identity::Instance()->hashCode($this->state);
		return array('stateHash');
	}
	
	public function __wakeup()
	{
		$this->state=Identity::Instance()->objectFromHashCode($this->stateHash);
	}
}

$object=new MyClass();
$clone=unserialize(serialize($object));

echo $object->state===$clone->state; //1
Itt a lényeg annyi, hogy lehetséges szerializált objektumokba külső referenciákat injektálni. Ehhez szükség van egy Map-re a referenciákról, amit jelen esetben az Identity singleton képvisel.

Ezzel a módszerrel lehetőség van többek között a rekurzió visszaállítására is, szóval meg lehet valósítani valami hasonlót, mint mondjuk ez:

save-cache.php:

$system=new System();
$system->setProperty($system);

$memento=$system->createMemento();
$memento->save('memento.php');
load-cache.php:

$memento=new Memento();
$memento->load('memento.php');

$newSystem=new System();
$newSystem->recover($memento);

assertEquals($newSystem, $newSystem->getProperty(), 'Visszaállt a rekurzió!');
A most bemutatott kód arra jó, hogy a manuális működést garantálja, viszont szeretnék olyan Serializer osztályt létrehozni, aminek elég megadni, hogy mik a külső hivatkozások, és automatikusan kicseréli őket. Na ez már sokkal keményebb dió...
 
1

Közben átgondoltam, hogy

inf · 2011. Nov. 18. (P), 09.11
Közben átgondoltam, hogy mindez proxy objektumok használatával megvalósítható, és nem kell gányolni a szerializált sztring forma átírásával, illetve sokkal egyszerűbb egy adott rész leválasztása ezzel a módszerrel. Mindjárt csinálok egy ilyen megoldást...

<?php

class Identity
{
    static protected $instance;
    static public function Instance()
    {
        if (!isset(self::$instance))
            self::$instance=new self();
        return self::$instance;
    }

    protected $selfHash;
    protected $map=array();

    public function __construct()
    {
        $this->selfHash=$this->hashCode($this);
    }

    public function identityHash()
    {
        return $this->selfHash;
    }

    public function hashCode($object)
    {
        $hashCode=spl_object_hash($object);
        $this->map[$hashCode]=$object;
        return $hashCode;
    }

    public function objectFromHashCode($hashCode)
    {
        return $this->map[$hashCode];
    }
}

class Proxy
{
    protected $target;
    protected $identityHash;
    protected $targetHash;
    
    static public function refersTo(Proxy $proxy)
    {
        return $proxy->target;
    }
    
    public function __construct($target)
    {
        $this->target=$target;
    }
    
    public function __call($method, $arguments)
    {
        return call_user_func_array(array($this->target, $method), $arguments);
    }
    
    public function __set($name, $value)
    {
        $this->target->$name=$value;
    }
    
    public function __get($name)
    {
        return $this->target->$name;
    }
    
    public function __sleep()
    {
        $this->targetHash=Identity::Instance()->hashCode($this->target);
        $this->identityHash=Identity::Instance()->identityHash();
        return array('targetHash', 'identityHash');
    }
    
    public function __wakeup()
    {
        if (Identity::Instance()->identityHash()===$this->identityHash)
            $this->target=Identity::Instance()->objectFromHashCode($this->targetHash);
        else
        {
            //injektálós rész
        }
    }
}

class State
{
    protected $value;

    public function __construct()
    {
        $this->value=rand(0,111);
    }
}

class MyClass
{
    public $state;
	
    public function __construct()
    {
        $this->changeState();
    }
	
    public function changeState()
    {
        $state=new State();
        $this->state=new Proxy($state);
    }
}

$object=new MyClass();
$clone=unserialize(serialize($object));

echo Proxy::refersTo($object->state)==Proxy::refersTo($clone->state); //1
Annyi a probléma ezzel az egésszel, hogy a __sleep és __wakeup metódusoknak nem lehet argumentumokat megadni, szóval muszáj globális változókon vagy osztályok statikus metódusain keresztül lekérni az identity Map-et, és az esetlegesen beinjektált dolgokat...

Egyébként ugyanezzel a módszerrel készíthető log a proxy-król, illetve lehetőség van a Proxy-k csoportosítására, és az egyes csoportoknál annak megadására, hogy a target-et szerializáljuk e avagy sem... A hátránya ennek a módszernek, hogy a típus vagy az interface teljesülését nem lehet ellenőrizni ezeknél az objektumoknál.
2

No közben még mindig ezen

inf · 2011. Dec. 28. (Sze), 05.55
No közben még mindig ezen agyalok :D Abban reménykedek, hogy proxy nélkül is meg lehet oldani, mert az eléggé lassít, meg a típusellenőrzés sem működik rá.

Amit találtam, az az, hogy van olyan, hogy Serializable interface. Ez egy fokkal szimpatikusabb, mint a sleep meg a wakeup, mert gyakorlatilag a createMemento és a restoreFromMemento ami a serialize és unserialize metódusok csinálnak, ha ezt az interface-t alkalmazzuk, tehát újrafelhasználható lesz a kód.

Amire még rájöttem, hogy ennél a típusú rekurziónál az a fő probléma, hogy a rekurzióban résztvevő objektumok egymáshoz képest milyen szinten vannak mondjuk ha egy fába rendezzük őket. Mondjuk tfh, van egy olyan rendszerünk, amiben van egy WebShop példány meg van egy DataBase és egy Session példány ezen belül. A Session tartalmaz egy Cart példányt. A Cart használja a DataBase-t.

Vizuálisan:
WebShop
-DataBase <--|
-Session     |
--Cart-------|
Nos ha a Session-t szerializáljuk, akkor a DataBase-t is szerializálni kell. Ilyenkor egy másik ágra mutatunk a fán, mint amin rajta vagyunk. Ez nem okoz rekurziót, viszont a DataBase példányosítását a WebShop kell, hogy végezze, nem pedig a Session... Tehát ha a DataBase-re hivatkozik a Session, akkor végső soron felesleges adatbázis kapcsolat fog létrejönni.

A másik lehetőség, hogy a WebShop-tól kéri el a Cart a DataBase-t:
WebShop   <--|
-DataBase    |
-Session     |
--Cart-------|
Így maradunk ugyanazon az ágon. Így már kialakul a klasszikus rekurzió, viszont ez már kezelhetőbb, mert a WebShop-ról tudjuk, hogy ő van a fa tetején, tehát ki tudjuk zárni a szerializálásból. Illetve ha minden DataBase-t és Session-t használó szerializálható objektumnak a WebShop-ot adjuk meg, mint referencia pontot, akkor a szerializálást úgy lehet automatizálni, hogy a WebShop ne kerüljön bele, akár Proxy-val, akár anélkül.

Ez nem rossz, viszont általában nem a WebShop-ra van szükségünk, hanem valamelyik alatta lévő erőforrásra. Ezért aztán minden egyes alkalommal, amikor arra a bizonyos erőforrásra akarunk hivatkozni végig kell menni az egész fenti fán, hogy elérjük.

Ehelyett érdemesebb talán minden egyes elemnek, ami a fán szerepel külön nevet adni, és azon keresztül elérni mondjuk singleton pattern-el...
pl:

class Cart
{
    public function save()
    {
        $database=Resources::Instance()->get('dataBase');
        //...
    }
}
Ezzel meg az a gond, hogy minden, de nem automatizálható, és egyáltalán nem kényelmes a használata, kód duplázódást okoz, mert minden metódus elején el kell kérni az adatbázis példányt...

Amit lehet tenni, hogy átemeljük ezt a kódot a sleep-be és a wakeup-ba:

class Cart
{
    protected $database;
    protected $others;
    
    public function save()
    {
        //...
    }
    
    public function __sleep()
    {
        return array('others');
    }
    
    public function __wakeup()
    {
        $this->database=Resources::Instance()->get('database');
    }
}
Ez még mindig nem az igazi, mert mindenhol egyesével meg kell adni, hogy mit szerializáljon a rendszer...

class Cart
{
    protected $database;
    protected $others;
    
    public function save()
    {
        //...
    }
    
    public function __sleep()
    {
		$properties=array_keys(get_object_vars($this));
		$serializedProperties=array_diff($properties, Resources::Instance()->getKeys());
        return $serializedProperties;
    }
    
    public function __wakeup()
    {
		$properties=array_keys(get_object_vars($this));
		$filteredProperties=array_diff($properties, $this->__sleep());
		foreach ($filteredProperties as $property)
			$this->$property = Resources::Instance()->get($property);
    }
}
Ez már egy fokkal jobb, viszont elég bajos a hibakezelése, illetve elég rugalmatlan a rendszer...
Helyette talán érdemesebb csinálni egy tömböt, amiben letároljuk, hogy milyen tulajdonságokat várunk vissza, illetve kell egy validator is, ami nézi ezek meglétét. Ez már inkább a Serializable interface-hez illő feladat szerintem, mert ott hozzá lehet adni extra tulajdonságokat a rendszerhez. Ennek mondjuk az a hátránya, hogy lassul a szerializálás és az utómunkák miatt a visszaállítás is.

Sokkal jobb lenne Proxy-val megoldani, mert azt az első hívásra lehet aktiválni, ha úgy szeretnénk:

class Proxy
{
	static protected $resources = array();

	protected $resource;
	protected $resourceClass;
	
	public function __construct($resource)
	{
		$this->resource = $resource;
		$this->resourceClass = get_class($this->resource);
		if (!isset(Proxy::$resources[$resourceClass]))
			Proxy::$resources[$this->resourceClass] = $this->resource;
	}
	
	public function __call($method, $arguments)
	{
		return call_user_func_array(array($this->resource, $method), $arguments);
	}
	
	public function __sleep()
	{
		return array('resourceClass');
	}
	
	public function __wakeup()
	{
		$this->resource = Proxy::resources[$this->resourceClass];
	}
}
Ez még az egyik legjobb megoldás, mert polimorfizmussal módosíthatjuk a Proxy viselkedését, hogy bizonyos erőforrásokkal kivételt tegyen ha azt szeretnénk, tehát könnyen módosítható. Ami érdekes, hogyha mondjuk csinálunk az erőforrásokhoz egy getProxy metódust, ami ugyanazt a Proxy példányt adja vissza, akkor a visszaállításnál már több ilyen proxy példány fog létrejönni.

Ami lényeges, hogy a szerializálásból kihagyott dolgokat is statikus Proxy tulajdonságra kell majd bíznunk... Az lenne a legjobb, ha a szerializálás vagy úgy menne, hogy Proxy::getSerializer() vagy Proxy::serialize()... Ebben az esetben a Proxy-t át kell nevezni mert nem az a fő feladata, hogy Proxy, hanem az, hogy a szerializálást segítse.

Egyelőre ennyi, ami eszembe jutott ezzel kapcsolatban, de még mindenképp tovább lesz gondolva... Például a Proxy-val lehetne akár Memento-ba menteni a tulajdonságokat a szerializáláskor, és később amikor már megvan a használt példány, akkor visszaállítani. Elég komoly lehetőségek vannak ebben a technikában...
3

Az ilyen bonyolultabb

virág · 2011. Dec. 28. (Sze), 09.19
Az ilyen bonyolultabb cachelési rendszerek megvalósítása közben nem árt teljesítmény teszteket is végezni, :) nehogy a végén legyen egy bonyolult, aprólékos cache alrendszered, amitől a rendszer lassabb lesz mintha nem cacheltél volna. Már láttam ilyet :). Amúgy tetszik.
4

Jah, én is gondoltam erre,

inf · 2011. Dec. 28. (Sze), 18.26
Jah, én is gondoltam erre, igazából ez akkor érné meg, ha jó sok parsolás meg builder pattern lenne, aminek csak az eredményét használja fel a rendszer, szóval mondjuk ha egy rakat konfig fájlból állna fel... Mindenképp tesztelni fogom kessel és anélkül. Ahol én sebességnövekedést remélek az az osztályok betöltése, mert úgy szeretném megoldani, hogy ezt az egészet összekötöm az autoloader-rel, és a felhasznált osztályokat is csatolom a cache fájlhoz. Így száz fájl helyett csak egyet kellene betölteni a rendszernek... Ez az osztály csatolásos dolog persze nem minden esetben megoldható, meg problémás, mert ha megpróbálok osztályt felüldefiniálni, akkor fatal error-t kapok... (Ami még szóba jöhet ezen kívül az a tisztán tartalom lekérő oldalak, azoknál SQL kéréseket lehet megspórolni...)

Egyelőre úgy vagyok vele, hogyha kikristályosodik ez a Proxy-s megoldás, akkor érdemes foglalkozni az osztályok csatolásával... Amit még korábban kigondoltam, hogy egy-egy oldal betöltését fel lehet osztani visszaállítási pontokra. Olyan a rendszerem, hogy először egy LibraryLoader áll fel (1.), utána egy ApplicationServer (2.), majd az ApplicationServer beszúrja a WebShop Application-t (3.), a WebShop Application átadja a kérést a Dispatcher-nek, ami létrehoz egy Controller-t (4.). A Controller beszélget a Model-el, és mondjuk az SQL szervertől választ kap (5.). A válasz alapján a View kimenetet generál (6.). A kimenetet átküldi a szerver a felhasználó böngészőjének (7.). Szóval 7 ponton biztosan lehet kesselni a kérés feldolgozását. Persze ahogy haladunk előre ebben, úgy ágazik el különböző esetekre a dolog, szóval az elején még könnyű kesselni, a végén viszont már bejönnek olyan függőségek, mint például a felhasználó jogosultsága, vagy a felhasználó neve, és így tovább... Az WebShop Application felállásáig (3.) elég egyszerű a dolgom, szóval azt a részt gond nélkül lehet kesselni, mert csak akkor változik, ha a php fájlok vagy a konfig fájlok megváltoznak...
5

Közben találtam kerülőutat

inf · 2011. Dec. 29. (Cs), 04.15
Közben találtam kerülőutat is. Csináltam egy ResourceRegistry nevű singleton-t, ahova a létrejöttekor beregisztrál minden erőforrás egy neki fenntartott néven. (Server, Application, DataBase, stb...)
Az őket használó objektumok konstruktorába pedig beteszem, hogy ebből a ResourceRegistry-ből kérjék le az erőforrás példányt. A sleep-et és a wakeup-ot manuálisan állítom be. Egyelőre ez egy áthidaló megoldás, mert most nincs időm általánosat csinálni. Ami biztos, hogy nem lehet megúszni singleton vagy globális változó nélkül... Én a singleton mellett döntöttem, mert sokkal nehezebb valakit rávenni arra, hogy állandóan ugyanazt a változónevet használja, mint arra, hogy ugyanazt az osztálynevet...

Igazából itt is a legjobb megoldás a Proxy (vagy most már Decorator-nak nevezem). Ha nincs ilyen, akkor mindenhol, be kell írni külön kódot a szerializáláshoz... Ennek a registry-s dolognak a nagy hátránya az, hogy minden "erőforrásból" csak egy példány lehet, ez meg rugalmatlanná teszi a kódot. Igazából ez a php nagy bullshit-je, hogy nem lehet paramétereket küldeni az unserialize-el a __wakeup-nak. Ha lehetne, akkor nem lenne szükség erre a workaround-ra...

Nagyon erős a késztetés, hogy csináljak ehelyett egy singleton Serializer osztályt, és attól kérjem el a paramétereket... Sőt 100%, hogy ez lesz. Utána azt hiszem írok egy cikket vagy blog bejegyzést erről az egészről.
6

Ahogy nézem vannak még vicces

inf · 2011. Dec. 29. (Cs), 06.50
Ahogy nézem vannak még vicces dolgok itt:

ini_set('unserialize_callback_func', 'spl_autoload_call');
Ha ez nincs bent, akkor nem tölti be automatikusan az osztályát a szerializált objektumnak. Alapból NULL-on van.

Tehát mindenképp kell egy Serializer osztály ahhoz, hogy normálisan menjen ez az egész...