Ne kartą kuriant tam tikras programas, man prireikė įvykiais (angl. ) paremto mechanizmo. Išbandžiau keletą būdų. Pavyzdžiui, įvykių ir klausytojų (angl. listeners) susiejimas tam tikru parametru, t.y. klausytojas gauna tik tuos įvykius, kurių ID sutampa su klausytojo ID. Tačiau čia mane pasitiko sąsajų (angl. em>interfaces) nelankstumo problema: negalėjau kurti klausytojų, kurių kiekvienas paveldi bazinę EventListener sąsają, su skirtingomis ID reikšmėmis. Taip atsitiko dėl to, kad ID reikšmę turėjau pasiimti per bazinį tipą, o sąsajų kintamieji privalo būti final. Taigi galėjo būti tik vienas klausytojo tipas, o to, žinoma, retai pakanka.

Kurdamas paprastą kliento-serverio programėlę galiausiai panaudojau paprastą būdą, kur yra tik vieno tipo įvykis:

public class Event 
{
    private String data;
    /** Konstruktorius */
    public Event(String data) {
        this.data = data;
    }
    public String getData(){
        return data;
    }  
    public void setInfo(String data){
        this.data = data;
    }
}

ir jam skirtas klausytojas:

public interface Listener
{   
    public void onEvent(Event e);
}

Žinoma, dar yra EventDispatcher, kuriam perduodami įvykiai, o jis skirsto tuos įvykius visiems paskelbtiems klausytojams. Tokiu būdu visi klausytojai gauna visus įvykius. Šis mechanizmas turbūt puikiausiai tiktų, jeigu tik nedidelė programos logikos dalis būtų paremta įvykiais. Tačiau kada ši dalis yra didesnė, atsiranda akivaizdžios problemos: įvykį apdorojantis metodas(šiuo atveju onEvent) turėtų atlikti įvykio analizę. Tam mums prireiktų į Event klasę pridėti, tarkime,

int eventType;

parametrą, ir pagal jį (jų gali prireikti ir daugiau, ar net tam tikros parametrų hierarchijos) metodas nusprenžia, kaip su įvykiu elgtis.

Principai

Kartą, žaisdamas su “ Reflection” sugalvojau, kaip būtų galima įdomiai išspręsti analizuojamą problemą. Įsivaizduokite:

  • Egzistuoja įvykiai, kurie gali laikyti kiek nori ir kokią nori informaciją bei netaikomi jokie apribojimai jų aprašui, išskyrus tą, kad jų klasės turi paveldėti bazinę Event ar jos išvestinę klasę.
  • Egzistuoja objektai, kurie gali gauti kokių nori tipų įvykius, bet privalo aprašyti (angl. implement) bazinę EventListener ar jos išvestinę sąsają.

Kuo čia dėtas Reflection? Paprasta: įvykių dispečeris gauna prašymą užregistruoti klausytoją. Peržiūri jame aprašytus metodus, ieško tarp jų argumentų tokių tipų, kurie būtų išvestiniai iš bazinės įvykio klasės. Jei tokių randa, tai įsimena, kad šis klausytojas norėtų gauti to tipo įvykius. Taigi gavęs įvykį dispečeris susiras tarp visų įregistruotų klausytojų tuos, kurie norį gauti šio tipo įvykius. Naudodamas Reflection iškvies atitinkamus kiekvieno klausytojo metodus. Skamba painiai, bet gal pasidarys kiek aiškiau iš žemiau pateiktos schemos:
Schema
Ją gal ne visai būtų galima vadinti sekų diagrama, bet apytikslis dėsningumas pranešimuose yra.

Iš schemos aišku, kad naujus įvykius statiškai talpinti eilėje gali bet kuris objektas, t.y. per EventDispatcher klasės vardą kviesdamas metodą fireEvent(Event e). Įvykis tokiu būdu vėliau bus paimtas dispečerio gijos (angl. DispatcherThread) ir apdorotas kuriant įvykių vykdytojus (angl. EventExecutor).

Klausytojai registruojami taip pat per dispečerio klasės vardą kviečiant addListener(EventListener l). Yra ir kitas būdas tai padaryti - kviečiame metodą addListener(l, ListenerServiseType). Tokiu kreipiniu galima aprašyti neblogą įvykių apdorojimo scenarijų: kiekvienas įvykis šiam klausytojui apdorojamas vienoje iš specialiai paskirtų gijų, t.y. iš ThreadPool. Tai naudinga, kai įvykius apdorojantys metodai atlieka daug laiko resursų reikalaujančias užduotis, tačiau dėl daugelio gijų reiktų pasirūpinti, kad negautumėme kritinės situacijos (angl. DeadLock). Tuo tarpu pirmasis būdas garantuoja, kad kiekvienas įvykis bus apdorotas dispečerio gijoje. Paprastai šią registraciją klausytojas savo konstruktoriuje atlieka pats.

Klausytojo pašalinimas nėra detalizuotas šioje schemoje. Iš esmės kviečiamas removeListener(EventListener l), o jame pereinami visi klausytojų sąrašai pagal įvykių tipus, kuriuos gauna pastarasis klausytojas (l) ir jis pašalinamas iš tų sąrašų. Tai supaprastintas paaiškinimas, bet visos detalės paaiškėja žvilgtelėjus i patį programos kodą.

Šiek tiek užsiminsiu ir apie klausytojų saugojimą dispečeryje. Tam naudojama žodyno tipo duomenų struktūra, kur raktažodis yra įvykio tipo vardas, pavyzdžiui, Event. Kiekvienam įvykiui žodyne saugoma po sąrašą klausytojų. Trumpai tariant, klausytojai yra suskirstyti į grupes pagal tai, kokius įvykius jie turėtų gauti. Iš čia seka tai, kad vienas klausytojas, gaunantis ne vieno tipo įvykius, atsiras žodyno keliuose sąrašuose. Beje, sąrašuose saugojami ne patys klausytojai, o jų deskriptoriai. Deskriptorius - tai struktūra turinti savyje nuorodas į klausytoją, jo apdorojimo tipą bei metodo aprašą, kurį reikia kviesti. Tokių deskriptorių sukuriama po vieną kiekvienam įvykius apdorojančiam metodui.

Kodas

Taigi dabar mano aprašyto mechanizmo klasės. Įdedu visų paketo failų turinį, jeigu kažkas iš tiesų norėtų tai kur nors panaudoti.

Klasė Event (tai bazinė įvykio klasė):

/*
 * Event.java
 *
 */
 
package events;
 
/**
 *
 * @author vigosas
 *
 * Bazinė Event klasė, visi kiti Event'ai privalo paveldėti šią klasę.
 * Jos gali turėti, kiek tik reikia laukų bei metodų.
 */
public class Event {
    private String data;
 
    /** Konstruktorius */
    public Event(String data) {
        this.data = data;
    }
 
    /** Grąžina bazinę informaciją */
    public String getData(){
        return data; 
    }
}

Ji saugo tik simbolių eilutę, bet pagal poreikius paveldinčios klasės gali turėti įvairius norimus laukus.

Bazinė EventListener sąsaja:

/*
 * EventListener.java
 *
 */
 
package events;
 
/**
 *
 * @author vigosas
 * 
 * Bazinė EventListener sąsaja. Visi kiti listeneriai privalo
 * paveldėti šią sąsają. Juose deklaruojami metodu karkasai, kur 
 * kiekvienas metodas gali turėti vieną parametrą - Bazinį ar kitą
 * Eventa. Panaudos pavyzdys:
 * 
 *   interface SomeEventListener extends EventListener
 *   {
 *       public void onSomeEvent(SomeEvent e);
 *       public void onAnotherEvent(AnotherEvent e);
 *   }
 *
 * Šią sąsają implementinantis tipas gaus SomeEvent ir AnotherEvent
 * tipų eventus. 
 */
 
/** Tuščias - visas reikalingas kodas bus paveldinčiose sąsajose*/
public interface EventListener {
 
}

Čia taip pat pateiktas pavyzdinės paveldinčios sąsajos aprašas (užkomentuotas).

Įvykių, perduotų apdoroti dispečeriui, eilė:

/*
 * EventQueue.java
 *
 */
 
package events;
 
import java.util.LinkedList;    
 
/**
 *
 * @author vigosas
 *
 * Eventų eilė, nauji Eventai patalpinami į jos galą, o dispečeris ima iš
 * pradžios.
 */
public class EventQueue {
 
    private  LinkedList queue; 
        //LinkedList panaudotas Event'ų saugojimui
    private  Object lock;
        //Sukuriamas bet koks objektas sinchronizavimui
 
    /** Konstruktorius */
    public EventQueue() {
        queue = new LinkedList();
        lock = new Object();
    }
 
    /** Metodas, skirtas Eventui pridėti */
    public void addEvent(Event e){
        synchronized(lock){
            queue.add(e);
        }
    }
 
    /** Metodas, skirtas pirmam eilėje Event'ui gauti */
    public Event getEvent(){
        synchronized(lock){
            if(queue.size() > 0)
                return(Event)(queue.removeFirst());
            else 
                return null;
        }        
    }
 
    /** Metodas grąžina true, jei eilė netuščia, priešingu atveju false */
    public boolean hasMoreEvents(){
        synchronized(lock){
           if(queue.size() > 0) return true;
           else return false;
        }        
    }
}

Eilei įgyvendinti paėmiau paprastą bazinį sąrašą. Įvykių įterpimas ir šalinimas yra sinchronizuotas, nes su eile vienu metu gali dirbti daugelis gijų.

ThreadPool:

/*
 * ThreadPool.java
 *
 */
 
package events;
 
import java.util.concurrent.*;
 
/**
 *
 * @author vigosas
 *
 * Klasė sukuria ThreadPool. Kai nėra užduočių (angl. tasks), tai laikomos
 * paleistos tik 5 gijos, prisireikus gali buti paleista iki 25
 */
public class ThreadPool {
    private final int MAX_THREAD_COUNT = 25;  //maksimalus veikiančių threadu kiekis
    private final int INITIAL_THREAD_COUNT = 5; //gijų kiekis, nors ir neturi darbo
    private final long KEEP_ALIVE = 5000;   //Idle threadai šalinami po 5 s.
    private ThreadPoolExecutor tpe;  //ThreadPool sukuriantis ir valdantis objektas
 
 
    /** Konstruktorius */
    public ThreadPool() {
        tpe = new ThreadPoolExecutor(
                INITIAL_THREAD_COUNT, 
                MAX_THREAD_COUNT, 
                KEEP_ALIVE, 
                TimeUnit.MILLISECONDS, 
                new LinkedBlockingQueue()
        ); //sukuriamias poolas
    } 
 
    /** Įdeda užduotį į iškvietimų eilę */
    public void ivokeLater(Runnable task){
        tpe.execute(task);
    }
}

ThreadPool gali paleisti iki 25 gijų, kuriose apdorojamas įvykių kodas.

Dispečeris:

/*
 * EventDispatcher.java
 *
 */
 
package events;
 
import java.util.*;
import java.lang.reflect.*;
 
/**
 *
 * @author vigosas
 *
 * Kontroliuoja visą eventų mechanizmą: laiko Listenerius ir Eventus (juos
 * skirsto Listeneriam).
 */
public class EventDispatcher {
 
    public enum ListenerServiseType{
        ExecuteInDispatcherThread, //EventListenerio eventai bus aptarnaujami dispečerio thredo
        ExecuteInSeparateThread    //EventListenerio evetai bus aptarnaujami vis naujo thread
    }
 
    private static EventQueue queue = new EventQueue();             //Event'u eilė
    private static Hashtable list = new Hashtable();                //Listeneriu sąrašų saugykla
    private static Object lock = new Object();                      //Listeneriu prieigos lockas           
    private static DispatcherTread dThread = new DispatcherTread(); //paleidžiamas dispečerio threadas
    private static LinkedList listenerCache = new LinkedList();     //detu Listeneriu keshas
 
 
    /** Prideda listenerį į listenerių konteinerį nustatant jo aptarnavimo būdą */
    public static void addListener(EventListener l, ListenerServiseType t) 
    {
        synchronized(lock){
            if(listenerCache.contains(l)) return;
            listenerCache.add(l);
                //kešuojama ir tikrinama tam, kad tas pats listeneris nebūtų įdėtas
                //ne vieną kartą, o tai sąlygotų pakartotinį eventų apdorojimą
            if(!EventListener.class.isAssignableFrom(l.getClass())) return;
            try{                    
                Method[] methods = l.getClass().getDeclaredMethods();
                for(Method method : methods){   //eina per visus listenerio metodus
                    method.setAccessible(true); //padaro prieinama
                    Class[] types  = method.getParameterTypes();
                    if(types != null)
                        if(types.length == 1) //vienam metodui - tik vienas eventas
                            if(Event.class.isAssignableFrom(types[0])){ 
                                    //patikrinama ar metodo parametras yra Event 
                                    //arba jį paveldinčio tipo
                                String eventType = types[0].getName();
                                LinkedList descriptors;
                                if(list.containsKey(eventType)){
                                    descriptors = (LinkedList)list.get(eventType);
                                }
                                else{
                                    descriptors = new LinkedList();
                                    list.put(eventType, descriptors);
                                }       //deskriptoriu sarasas
                                ListenerDescriptor descriptor = 
                                        new ListenerDescriptor(l, t, method);
                                descriptors.add(descriptor); //prideda nauja deskriptoriu
                            }
                }
            } catch(Exception e){e.printStackTrace();}
        }    
    }
 
    /** 
     * Prideda listenerį į listenerių konteinerį. Jis bus aptarnautas
     * numatytuoju būdu, t.y. Event'u apdorojimo kodas bus vykdomas
     * EventDespatcher'io gijoje
     */
    public static void addListener(EventListener l){
        addListener(l, ListenerServiseType.ExecuteInDispatcherThread);
    }
 
    /** 
     * pašalina listenerį
     * PASTABA! Pašalinti listenerį yra BŪTINA, nes to nepadarius,
     * objekto Event kodas ir toliau bus kviečiamas, nors ir neliks 
     * jokiu aktyvių nuorodų į jį (t.y. liks tik šitas; Garbage Collector 
     * negalės Listenerio sunaikinti)
     */
    public static void removeListener(EventListener l){
        synchronized(lock){
            try{                    
                Method[] methods = l.getClass().getDeclaredMethods();
                for(Method method : methods){   //eina per visus listenerio metodus
 
                    Class[] types  = method.getParameterTypes();
                    if(types != null)   
                        if(types.length == 1){  
                             String eventType = types[0].getName();
                             LinkedList descriptors = (LinkedList)list.get(eventType);
                             for(Object o : descriptors){ 
                                    //šalina visus deskriptorius
                                    //šiam listeneriui                    
                                 ListenerDescriptor descriptor = (ListenerDescriptor)o;
                                 if(descriptor.l == l){
                                     descriptors.remove(descriptor);
                                     break;
                                 }
                             }
                        }
                }
            } catch(Exception e){e.printStackTrace();}
        }         
    }
 
    /** patalpina Event'a i eilės galą apdorojimui */
    public static void fireEvent(Event e){
        queue.addEvent(e);
    }
 
    /*************************************************************************** 
     *                   Gija, paskirstanti visus eventus                      *
     **************************************************************************/
    private static class DispatcherTread extends Thread {
        /** Konstruktorius */
        public DispatcherTread(){
            this.start(); //inicializuojant paleidzia save
        }
 
        /** uzklotas klases Thread metodas, kur veiksmas persikelia i naują gija :) */
        public void run(){
            EventQueue queue = EventDispatcher.queue;
            while(true){
                try{
                    while(!queue.hasMoreEvents()){
                        sleep(1);
                        //kad "nevalgytų" procesoriaus, gija užmigdoma vienai
                        //milisekundei, vos tik eventų eilė pasidaro tuščia
                    }
                    Event event = queue.getEvent();
                    if(event != null){
                        LinkedList descriptors = 
                                (LinkedList)EventDispatcher.list.get(event.getClass().getName());
                        if(descriptors != null){
                            for(Object o : descriptors){
                                new EventExecutor((ListenerDescriptor)o, event).execute();                            
                            }
                        }
                    }
                } catch(Exception e){e.printStackTrace();}
            }
        }
    }
}

Dispečeris inicializuoja eilę, klausytojų sąrašus, ThreadPool ir dispečerio giją, kuri aprašyta tame pačiame faile. Jis suteikia sinchronizuotą klausytojų pridėjimą ir šalinimą.

Įvykio vykdytojas (EventExecutor):

/*
 * EventExecutor.java
 *
 */
 
package events;
 
import java.lang.reflect.*;
 
/**
 *
 * @author vigosas
 *
 * Paleidžia Event apdorojimo kodą, pagal desktiptoriaus parametro
 * EventDispatcher.ListenerServiseType reikšmę kodas vykdomas EventDispatcher
 * arba atskiroje gijoje iš ThreadPool (tai naudinga tuo atveju, kai
 * eventai apdoroja daug kodo, kadangi priešingu atveju būtų labai
 * apkrautas EventDispatcher threadas). Vis dėlto, to reiktų vengti, jei
 * skirtingose gijose apdojomi objektai turi bendrų resursų, gali 
 * būti DeadLock'as. 
 */
public class EventExecutor{
    private ListenerDescriptor desc; 
    private Event e;
    private static ThreadPool pool = new ThreadPool();
 
    /** Konstruktorius */
    public EventExecutor(ListenerDescriptor desc, Event e) {
        this.e = e;
        this.desc = desc;
    }
 
    /** vykdo paleidimą sinchroninį arba asinchroninį (pagal listenerio deskriptorių) */
    public void execute(){
        if(e==null  || desc==null) return;
        if(desc.t == EventDispatcher.ListenerServiseType.ExecuteInDispatcherThread)
            executeEvent(); //leidziam dispecerio threade
        else
            executeInPool(); //leidziama pool'o viename is thread'u
    }
 
    /** Duoda užduoti įvykdyti ThreadPool'ui */
    private void executeInPool(){
        pool.ivokeLater(new Runnable(){
           public void run(){
               executeEvent();
           } 
        });
    }
 
    /** Leidžia evento apdorojimo kodą */
    private void executeEvent(){
        try{
            desc.m.invoke(desc.l, new Object[]{e}); 
                //paleidziamas Event apdorojimo kodas listeneryje
                //(per Reflection)
        } catch(Exception e){e.printStackTrace();}
    }
}

Pats vykdytojas pagal deskriptoriaus parametro ListenerServiseType reikšmę vykdo įvykio apdorojimo kodą, t.y. DispatcherThread gijoje arba vienoje iš ThreadPool gijų asinchroniškai.

Ir jau mano minėtas klausytojo deskriptorius:

/*
 * ListenerDescriptor.java
 *
 */
 
package events;
 
import java.lang.reflect.*;
 
/**
 *
 * @author vigosas
 *
 *  Vienas Listeneris gali turėti kelis deskriptorius - po vieną
 *  kiekvienam metodui, kuris sužadinamas evento.
 */
public class ListenerDescriptor {
    public EventDispatcher.ListenerServiseType t;   //apdorojimo budas
    public EventListener l;                         //listeneris
    public Method m;                                //metodas
 
    /** Konstruktorius */
    public ListenerDescriptor(EventListener l, EventDispatcher.ListenerServiseType t, Method m) {
        this.l = l;
        this.t = t;
        this.m = m;
    }
}

Kaip jau minėjau, kiekvienam klausytojo metodui sukuriama po vieną deskriptorių.

Nesigilinant į visą pateiktą kodą, vieninteliai metodai, kuriuos jums reikėtų žinoti (ir tik juos kviesti), jei norite naudoti šį mechanizmą tai:

  • Klausytojo pridėjimas: EventDispather.addListener(EventListener l)
  • Klausytojo pašalinimas EventDispather.removeListener(EventListener l)
  • Naujo įvykio įterpimas: EventDispather.fireEvent(Event e)

Pavyzdžiai

Tikiu, kad ir ilgokai paanalizavus šį kodą būtų sunku pradėti naudoti šį paketą (aš jį savo projekte pavadinau ““), todėl pateiksiu paprastą pavyzdį:

/*
 * Main.java
 *
 */
 
package main;
 
import events.*;
 
/**
 *
 * @author vigosas
 */
public class Main {    
    public static void main(String[] args){
       new EventAdapter();
       new AnotherAdapter();
       EventDispatcher.fireEvent(new SomeEvent("this is some event"));
       EventDispatcher.fireEvent(new AnotherEvent("this is another event"));        
    }
}
 
 
class SomeEvent extends Event
{
    public SomeEvent(String data){
        super(data); 
    }
}
 
class AnotherEvent extends Event
{
    public AnotherEvent(String data){
        super(data);
    }
}
 
interface SomeEventListener extends EventListener
{
    public void onSomeEvent(SomeEvent e);
    public void onAnotherEvent(AnotherEvent e);
}
 
class EventAdapter implements EventListener
{
    public EventAdapter(){
        EventDispatcher.addListener(this);
    }
    public void onSomeEvent(SomeEvent e){
        System.out.println("onSomeEvent at EventAdapter");
        System.out.println(e.getData());
    }
 
    public void onAnotherEvent(AnotherEvent e){
       System.out.println("onAnotherEvent at EventAdapter");
       System.out.println(e.getData());
    }
}
 
class AnotherAdapter implements SomeEventListener
{
    public AnotherAdapter(){
        EventDispatcher.addListener(this, 
                EventDispatcher.ListenerServiseType.ExecuteInSeparateThread);
    }
    public void onSomeEvent(SomeEvent e){
        System.out.println("onSomeEvent at AnotherAdapter");
        System.out.println(e.getData());
    }
 
    public void onAnotherEvent(AnotherEvent e){
       System.out.println("onAnotherEvent at AnotherAdapter");
       System.out.println(e.getData());
    }    
}

Pirmiausia importuojame paketą, kur sudėtos visos įvykių mechanizmo klasės, tam skirta ši eilutė:

import events.*;

Tada apsirašome du naujus įvykių tipus: SomeEvent ir AnotherEvent. Dabar susikursime sąsają - karkasą klasės, kuri apdoros abiejų tipų įvykius. Ją pavadinau SomeEventListener, be abejo, ji privalo paveldėti EventListener. Čia aprašau metodus, gausiančius aukščiau paskelbtų tipų įvykius. Dar seka du adapteriai, aprašantys (implements) SomeEventListener. Iš pirmo žvilgsnio jie gali pasirodyti vienodi, tačiau esminis skirtumas yra konstruktoriuje. Pirmojo adapterio konstruktoriaus kodas:

EventDispatcher.addListener(this);

antrojo:

EventDispatcher.addListener(this, 
        EventDispatcher.ListenerServiseType.ExecuteInSeparateThread);

Pirmojo adapterio onSomeEvent(SomeEvent e) ir onAnotherEvent(AnotherEvent e) metodai bus vykdomi DispatcherTread gijoje, o antrojo analogiški metodai - kažkurioje(se) iš ThreadPool gijų t.y. asinchroniškai. Šį būdą pritaikiau remdamasis .NET analogija, kur asinchroniniai delegatai vykdomi specialiame ThreadPool.

Paleidus programą, ji išveda tokį rezultatą:

onSomeEvent at EventAdapter
this is some event
onAnotherEvent at EventAdapter
this is another event
onSomeEvent at AnotherAdapter
this is some event
onAnotherEvent at AnotherAdapter
this is another event

Jei šios eilutės susikeistų vietomis, tai būtų normalu, kadangi priverčiame dirbti ne vieną giją.

Dar vienas įdomus ir, ko gero, patogus faktas yra tai, kad iš šio pavyzdžio galime išmesti visą SomeEventListener aprašą, o adapterių antraštėse implements SomeEventListener pakeisti į implements EventListener. Gausime tą patį rezultatą. Kaip taip gali būti? Paprastai: adapteriai paveldės EventListener, o to pakanka, kad EventDispatcher’is galėtų su juo dirbti. Žinoma, remiantis nuoseklaus projektavimo principais, būtų geriau susikurti sąsajas, kaip parodžiau aukščiau.

Baigiamosios mintys

Mano pateiktas problemos sprendimo būdas nebūtinai yra optimalus, o gal net palikau klaidų, kurios gali priversti programą dirbti nenuspėjamai ar iš viso nedirbti. Tokiu atveju, norėčiau sulaukti atsiliepimų ir komentarų. Be abejo, norėčiau gauti patarimų, kaip mano modelį būtų galima patobulinti ar pataisyti.

Panašūs straipsniai


“Universalus įvykiais paremtos programos mechanizmas” komentarų: 2

  1. ¸Giedrius

    Labai primityvus aprasymas…

  2. Tomas

    Kaip programuotojas galiu paskyt, jog viskas puikiai išdėstyta ;)

Rašyti komentarą

Jūs privalote prisijungti jeigu norite rašyti komentarą.