janos.szurovecz.hu

A régi blogom, ahova lehet írok majd még...

Intelligens eseménykezelés

2013-06-07 5 perc olvasási idő

Manapság minden valamirevaló keretrendszerben elérhető valamilyen eseménykezelő, ami a publish/subscribe mintát követi. Ezekkel az eszközökkel flexibilisebb alkalmazásokat írhatunk, alacsonyabb lesz a “coupling” az osztályain között.

Lehetőségek tárháza…

Egy ilyen eseménykezelőt akartam volna integrálni DDD-s környezetbe. Megnéztem, milyen lehetőségeim vannak és arra jutottam, hogy többé-kevésbé ugyanazt tudja minden framework. De nézzünk megy egy példát a Symfony-ból:

$listener = new AcmeListener();  
$dispatcher->addListener('foo.action', array($listener, 'onFooAction'));

Magát az eventet egy stringgel azonosítjuk, jelen esetben ‘foo.action’-nel. Valamint explicit módon megadjuk, hogy az említett azonosítójú eventeket a $listener objektum ‘onFooAction’ metódusa tudja lekezelni. Ezzel nekem van egy óriási problémám: a lehetőség, hogy elgépelem akár az event, akár a metódus nevét. Ráadásul mi történik egy refaktorálás során? Semmi, észben kell tartani, hogy hohó, ott is át kell ám írni a stringet!

Gondoltam egyet, és megnéztem pár javas event bus megvalósítást, nevesül a Google féle Guava projektet, valamint az Axon frameworkben lévőt. Mindkettőből merítettem ötleteket (előbbiből talán többet) és nekiláttam PHP-ben. Így született a predaddy.

Eventek

Az eventek objektumok. Az event interfészt kell implementálnunk. Technikai és kényelmi okokból ez az ObjectInterface-re épül, amelyből a java-s Object osztályból ismert jónéhány metódus köszön vissza. Ezeket implementálhatjuk mi magunk is, de egyszerűbb az Object osztályból származtatnunk az event osztályunkat. Ha még lustábbak vagyunk, az EventBase-t is felhasználhatjuk, amiben minden szükséges metódus implementálva van. Egy szó mint száz, az eventek szerializálhatók, azonosítóval vannak ellátva, valamint tartalmaznak egy időbélyeget a létrehozásuk idejéről.

EventHandler

Az események kezeléséhez az EventHandler interfészt kell implementálnunk. Az viszont üres! A eseményeket lekezelő metódusainkat úgy nevezzük el, ahogy szeretnénk, nincs megkötés. Ráadásul egy EventHandler osztályban több ilyen metódus is szerepelhet, amelyek eltérő eseményeket is le tudnak kezelni. És hogy mondjuk meg az event busnak, hogy mely eseményre mely metódust hívja? Annotációval. A predaddy event bus a regisztrált event handlerek metódusait scanneli, és értelmezi őket. Nekünk mindössze a @Subscribe annotációval kell ellátnunk a metódusainkat, paraméterül pedig egy typehinttel ellátott eventet várunk. Ezen typehint alapján fogja tudni az event bus, hogy az adott metódus mely eventet tudja feldolgozni.

/** 
 * Egy event osztály 
 */  
class UserRegistered extends EventBase  
{  
    protected $email;  
    public function __construct($email)  
    {  
        parent::__construct();  
        $this->email = $email;  
    }  
    public function getEmail()  
    {  
        return $this->email;  
    }  
}  
/** 
 * Eseménykezelő osztály egy eseménykezelő metódussal 
 */  
class EmailSender extends Object implements EventHandler  
{  
    /** 
     * Az annotáció és a typehint alapján az eventbus tudja, 
     * hogy ez egy eseménykezelő metódus, amit a UserRegistered és 
     * annak minden származtatott osztályának példányára meg kell hívnia. 
     * 
     * @Subscribe 
     */  
    public function sendMail(UserRegistered $event)  
    {  
        printf("Sending email to %s...n", $event->getEmail());  
    }  
}  
// event bus inicializálás  
$bus = new DirectEventBus('sample');  
$bus->register(new EmailSender());  
// eventek küldése  
$bus->post(new UserRegistered('example1@example.com'));  
$bus->post(new UserRegistered('example2@example.com'));  
$bus->post(new UserRegistered('example2@example.com'));

Event typehint

Az Event osztályainkat tetszőleges módon származtathatjuk egymásból, komplex hierarchiát alakíthatunk ki. A predaddy az elvárt módon kezeli akár az absztrakt, akár az interfész typehintet is. Vagyis ha egy absztrakt osztályból származó összes event típust fel szeretnénk dolgozni egy eseménykezelőben, akkor nem kell minden altípusra létrehozni egy metódust, hanem elég egyet, amiben az event-ök ősosztályát adjuk meg. Tehát ha az “Event” típust adjuk meg egy eseménykezelő metódusnál, akkor az minden eseményre meg fog hívódni.

DeadEvent - ha valamit nem kezeltünk le

Előfordul, hogy szándékosan, vagy valamilyen hiba hatására egy eseményhez nem regisztráltunk egy eseménykezelőt sem. Ebbent az esetben a predaddy becsomagolja azt egy DeadEvent objektumba és elküdi a handlereknek. Érdemes például loggolási célból egy DeadEvent handlert beregisztrálni, így könnyebb a hibakeresés.

Aszinkron és tranzakciófüggő események

PHP-ben sajnos nem lehet többszálú programokat írni, bár egyesek szerint ez talán jól is van így. A lényeg, hogy ily módon nem egyszerű aszinkron végrehajtást elérni. Viszont az mf4php-ben már megoldást adtam az említett problémákra, így készítettem egy Mf4PhpEventBus-t, ami kihasználja az abban lévő lehetőségeket. Ez az implementáció önmagában egy MessageListener is, ami arra a queue-ra iratkoztatja fel magát, amire az eseményeket is küldi. Vagyis a predaddy-s eseményeket mf4php-s Message-be rakja, azt elküldi az mf4php-nek, az visszaküldi a busnak és az továbbitja őket az eseménykezelőknek. Ezzel az mf4php-s kitérővel elérhető az, hogy az üzeneteink akár tranzakciókhoz legyenek kötve, akár aszinkron módon kerüljenek feldolgozásra. Utóbbihoz a beanstalk implementációt ajánlom, ami jelenleg az egyetlen megoldás (szívesen veszek egyéb mf4php implementációkat is).

Mint említettem, az mf4php az eventeket Message-be csomagolja. A konkrét mf4php implementáció támogathat extra funkciókat, mint például késleltetés, priorizálás, időlimitáció. Saját ObjectMessageFactory implementációval beleszólhatunk ebbe a folyamatba és adott event típusonként elkészíthetjük mi magunk is a kívánt Message objektumot.

Aggregate root - DDD

A predaddy-ben lévő event bus nem csak DDD-s projektben használható, de biztosít számunkra néhány plusz osztályt ha mégis DDD-t használunk. Az aggregate root osztályokat származtassuk az AggregateRoot osztályból és statikus módon adjuk át az osztálynak a használni kívánt event bust. A statikus raise() metódussal küldhetünk az osztályon belülről eventeket, melyeket a DomainEvent osztályból kell származtatnunk. Ilyen felhasználás esetén mindenképp javaslom az Mf4PhpEventBus-t valamilyen TransactedMessageDispatcher-rel karöltve, hogy az elsütött eventeket csak és kizárólag sikeres tranzakció után dolgozzuk fel.

Végszó

Meg kell jegyeznem, hogy a predaddy - mivel annotációt használ - erősen támaszkodik a reflectionre. Vagyis valószínűleg lassabb, mint az említett Symfony-s módszer. Bár teljesítmény teszteket nem futtattam, véleményem szerint olyan kicsi lehet ez az overhead, hogy nekem megéri a kényelmesebb, és hibamentesebb megoldás. Remélem nektek is! :)

comments powered by Disqus