Implementing a Pain-Free, Type Safe Event Engine

jonas

Ultra Cool Member
Reaction score
47
Task: Implement the Warcraft 3 event engine in C# or Java in a type safe, cohesive way, with low coupling
Problem: Different events have different data, so you need a way to distinguish between different events, handlers for different events, etc.


Slang:
interface IEvent (all events are objects of types that implement this interface)
interface IEventListener<E> where E : IEvent { void Handle(E event); }

or

interface IEventListener { void Handle(IEvent event); }
(listens to events of a specific type)

class Dispatcher { void Register???(IEventListener???); void Dispatch(IEvent e); }
(can register event listeners and broadcast events in an efficient manner).


Known Non-Solutions:
  • Cluttering the event type with all the accessors (violates type safety of the event)
  • Having a generic IEventListener<E> where E : IEvent type (then the dispatcher needs ListenersE1 : List<IEventListener<E1>>, ListenersE2 : List<IEventListener<E2>>, ..., because you can't mix instances of generics in a list - and whenever you create a new event, you have to change the code of the dispatcher in trivial ways)
  • Having IEvent with no accessors, IEventListener with method Handle(IEvent e) (because you lose type safety of the event, and you need to type check and cast inside of the Handle())
  • Having Handle<E>(E event), (because in the implementation of Handle you can't really know which type the event really is, so you can't access its fields etc.)
  • Implementing one HandleE method for each event type, because then whenever you want to add a new event you have to touch the IEventListener interface (or better, make it an abstract class with default No-Op behaviours; otherwise you have to touch all the implementations as well)
Solutions that don't work because of the type system:
  • Having Handle<E>(E event) and checking E at compile time with static if, implementing it as a no-op in "wrong" types
  • Allowing mixed generics in a list, and comparing the types
  • Using types like enums
  • ...
 
Last edited:

Accname

2D-Graphics enthusiast
Reaction score
1,456
How about something like this? (Note: I tried to keep it simple)
Code:
class EventDispatcherSystem {
 
   private final Map<Class<? extends Event>, EventDispatcher<?>> dispatchers = new HashMap<>();
 
   public <K extends Event> void fireEvent(K event) {
     @SuppressWarnings("unchecked")
     EventDispatcher<K> d = (EventDispatcher<K>) dispatchers.get(event.getClass());
     d.fireEvent(event);
   }
 
   public <K extends Event> void addEventListener(EventListener<K> listener) {
     Class<K> eventClass = listener.getEventClass();
     @SuppressWarnings("unchecked")
     EventDispatcher<K> d = (EventDispatcher<K>) dispatchers.get(eventClass);
     if (d == null) {
       d = new EventDispatcher<>();
       dispatchers.put(eventClass, d);
     }
     d.addEventListener(listener);
   }
 
}

class EventDispatcher<K extends Event> {
 
   private final List<EventListener<K>> listeners = new ArrayList<>();
 
   public void fireEvent(K event) {
     for (EventListener<K> l : listeners) {
       l.onEvent(event);
     }
   }
 
   public void addEventListener(EventListener<K> listener) {
     listeners.add(listener);
   }
 
}

interface Event {
   // Could have some kind of functionality
}

interface EventListener<E extends Event> {
 
   public void onEvent(E event);
 
   public Class<E> getEventClass();
 
}
You have one EventDispatcherSystem where you can register EventListener's and you can also fire Event's. Its pretty type safe except for 2 unchecked type casts which can not really be avoided in java. But we know for sure that these are safe because we make sure when registering EventListener's the generic types of the listener and the dispatcher fit each other. This compiles without problems on java. I have currently no C# compiler at hand but I am pretty sure you can do the same thing with C#.



Edit:
Here is also an example of how this can be used:
Code:
class UseCase {
  
   public static void main(String[] args) {
     EventDispatcherSystem eds = new EventDispatcherSystem();
    
     eds.addEventListener(new EventListener<StringEvent>() {
      
       public void onEvent(StringEvent event) {
         System.out.println("String: "+event.getValue());
       }
      
       public Class<StringEvent> getEventClass() {
         return StringEvent.class;
       }
     });
    
     eds.fireEvent(new StringEvent("ABC"));
    
     eds.fireEvent(new StringEvent("123"));
   }
  
}
 
Last edited:

jonas

Ultra Cool Member
Reaction score
47
Yes, I have also looked at a similar solution (mapping from Type to dispatcher), but couldn't find an equivalent for
Code:
 Class<? extends Event>, EventDispatcher<?>
in C#. Otherwise I could just do List<EventDispatcher<?>> , and already resolve the issues above.

This smells to me:
Code:
 dispatchers.get(event);
I know that a compiler can get this to work, but I would have expected something along the lines of event.getClass(), but I'm not so familiar with Java.

Thanks for your response, I'll see if C# also has these unspecified generics, or whatever is the appropriate search term. I find it hard to google for such things.
 

Accname

2D-Graphics enthusiast
Reaction score
1,456
Oh, that was indeed an error on my part. I didnt test it, I only compiled it. Stupid java doesnt check type for the get method.
 

jonas

Ultra Cool Member
Reaction score
47
Oh, that was indeed an error on my part. I didnt test it, I only compiled it. Stupid java doesnt check type for the get method.
May I jokingly remind you that the whole discussion was about a type safe implementation ;)
 

Accname

2D-Graphics enthusiast
Reaction score
1,456
By the way, one nice feature of this solution is that you can change the current EventDispatcher class into an EventDispatchStrategy interface and then implement different EventDispatchStrategy's as you need. You can even combine it with a factory pattern to change strategy implementations on the fly. This will give ou maximum flexibility and reusability.

One disadvantage of the current implementation is the fact, that sub-classes of event types will not be delivered to listeners for super classes.
For example: If we have the class StringEvent and then a subclass TimeSensitiveStringEvent. Then an EventListener<StringEvent> will not be notified of TimeSensitiveStringEvent's even though one would intuitively think they would.
This is why I would recommend, depending on the applications specific requirements, to iterate over all super classes of the event and notify all EventDispatcher's for all these types.

You could also improve the system to be safe against the lapsed observer problem (Link: https://en.wikipedia.org/wiki/Lapsed_listener_problem ). For this I would suggest using WeakReference within the List of EventDispatcher. You could also use a WeakHashMap within the EventDispatcherSystem.
 

jonas

Ultra Cool Member
Reaction score
47
I'm afraid in C# you really can't write down a type like that one. You have to put object as the domain of the map. But it won't make a big difference, since you use unsafe type casts anyways.

PS: to explain what I meant by more expressive type system: in Coq you could write
forall E : Type, extends E Event -> EventDispatcher E

In such a setting you really can get a type safe variant, where the correctness doesn't rely on the Programmer only using the map in a specific way. Which was what was originally the point of debate.

By the way, one nice feature of this solution is that you can change the current EventDispatcher class into an EventDispatchStrategy interface and then implement different EventDispatchStrategy's as you need. You can even combine it with a factory pattern to change strategy implementations on the fly. This will give ou maximum flexibility and reusability.
Not necessary, this thing is completely fixed.

One disadvantage of the current implementation is the fact, that sub-classes of event types will not be delivered to listeners for super classes.
For example: If we have the class StringEvent and then a subclass TimeSensitiveStringEvent. Then an EventListener<StringEvent> will not be notified of TimeSensitiveStringEvent's even though one would intuitively think they would.
This is why I would recommend, depending on the applications specific requirements, to iterate over all super classes of the event and notify all EventDispatcher's for all these types.
I'm aware of this but it is a yagni issue. I can't come up with a use case for event subclasses. Besides, you can just register yourself for all of the subclasses (C# can hopefully do this using contravariance with no extra effort).

You could also improve the system to be safe against the lapsed observer problem (Link: https://en.wikipedia.org/wiki/Lapsed_listener_problem ). For this I would suggest using WeakReference within the List of EventDispatcher. You could also use a WeakHashMap within the EventDispatcherSystem.
Listeners are actively managed in the program, so this is not an issue.
 

Accname

2D-Graphics enthusiast
Reaction score
1,456
Okay, here is a solution which uses the Visitor pattern to solve your buffered event problem: (This example is starting to become really really big!)
Code:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class UseCase {

   public static void main(String[] args) {
     EventDispatcherSystem eds = new EventDispatcherSystem();
     eds.addEventListener(new EventListener<StringEvent>() {
    
       public void onEvent(StringEvent event) {
         System.out.println("StringEvent: "+event.getValue());
       }
    
       public Class<StringEvent> getEventClass() {
         return StringEvent.class;
       }
     });
  
     eds.bufferEvent(new StringEvent("Test"));
     eds.dispatchAllEvents();
   }

}

class EventDispatcherSystem {

   private final Map<Class<? extends Event>, EventDispatcher<?>> dispatchers = new HashMap<>();
   private final List<Event> eventBuffer = new ArrayList<>();

   public void bufferEvent(Event event) {
     eventBuffer.add(event);
   }

   public void dispatchAllEvents() {
     for (Event ev : eventBuffer) {
       ev.dispatch(this);
     }
     eventBuffer.clear();
   }

   public <K extends Event> void dispatchEvent(K event) {
     @SuppressWarnings("unchecked")
     EventDispatcher<K> d = (EventDispatcher<K>) dispatchers.get(event.getClass());
     d.fireEvent(event);
   }

   public <K extends Event> void addEventListener(EventListener<K> listener) {
     Class<K> eventClass = listener.getEventClass();
     @SuppressWarnings("unchecked")
     EventDispatcher<K> d = (EventDispatcher<K>) dispatchers.get(eventClass);
     if (d == null) {
       d = new EventDispatcher<>();
       dispatchers.put(eventClass, d);
     }
     d.addEventListener(listener);
   }

}

class EventDispatcher<K extends Event> {

   private final List<EventListener<K>> listeners = new ArrayList<>();

   public void fireEvent(K event) {
     for (EventListener<K> l : listeners) {
       l.onEvent(event);
     }
   }

   public void addEventListener(EventListener<K> listener) {
     listeners.add(listener);
   }

}

interface Event {
   public void dispatch(EventDispatcherSystem eds);
}

interface EventListener<E extends Event> {

   public void onEvent(E event);

   public Class<E> getEventClass();

}

class StringEvent implements Event {

   private final String value;

   public StringEvent(String value) {
     this.value = value;
   }

   public String getValue() {
     return value;
   }

   public void dispatch(EventDispatcherSystem eds) {
     eds.dispatchEvent(this);
   }

}
The import part is now that the Event interface has a method "public void dispatch(EventDispatcherSystem eds) " which will be called by the EventDispatcherSystem. The implementation for the public "void dispatch(EventDispatcherSystem eds)" will be the very same for every implementing class of Event. They will call the "dispatchEvent(K)" method of the EDS with "this" as argument. Because each Event implementation knows its own type at compile time this will become type safe!


The above code compiles and works as intended (I tested it). You can copy + paste it into one file called UseCase.java. You should be able to compile and run it right away.
 
Last edited:

CandiceRoorm

New Member
Reaction score
0
Looks like dy/dx is becoming ex in the case of our dollar. I think if we used up all the Tech skills here in JA create a silicon island we could turn this around ??? :
 
General chit-chat
Help Users
  • No one is chatting at the moment.
  • The Helper The Helper:
    Happy Sunday!
    +1
  • The Helper The Helper:
    I will be out of town until Sunday evening
    +1
  • The Helper The Helper:
    I am back! Did you miss me LOL
    +1
  • jonas jonas:
    where did you go?
  • The Helper The Helper:
    Jefferson TX on a Paranormal Investigation of a haunted bed and breakfast - I got some friends that are paranormal investigators and they have an RV and do YouTubes
    +1
  • The Helper The Helper:
    It was a lot of fun. The RV was bad ass
  • jonas jonas:
    That sounds like fun!
    +1
  • The Helper The Helper:
    it was a blast!
  • The Helper The Helper:
    I am going to post the Youtube of the investigation in the forums when it is ready
    +1
  • jonas jonas:
    cool!
  • vypur85 vypur85:
    Sounds cool TH.
  • tom_mai78101 tom_mai78101:
    I was on a Legend of Zelda marathon...
  • tom_mai78101 tom_mai78101:
    Am still doing it now
    +1
  • jonas jonas:
    which one(s) are you playing?
  • jonas jonas:
    I played a little bit of the switch title two weeks ago and found it quite boring
  • The Helper The Helper:
    just got back from San Antonio this weekend had the best Buffalo Chicken Cheesesteak sandwhich in Universal City, TX - place was called Yous Guys freaking awesome! Hope everyone had a fantastic weekend!
    +1
  • The Helper The Helper:
    Happy Tuesday!
  • The Helper The Helper:
    We have been getting crazy numbers reported by the forum of people online the bots are going crazy on us I think it is AI training bots going at it at least that is what it looks like to me.
  • The Helper The Helper:
    Most legit traffic is tracked on multiple Analytics and we have Cloud Flare setup to block a ton of stuff but still there is large amount of bots that seem to escape detection and show up in the user list of the forum. I have been watching this bullshit for a year and still cannot figure it out it is drving me crazy lol.
    +1
  • Ghan Ghan:
    Beep boop
    +1
  • The Helper The Helper:
    hears robot sounds while 250 bots are on the forum lol
  • The Helper The Helper:
    Happy Saturday!
    +1

    The Helper Discord

    Members online

    No members online now.

    Affiliates

    Hive Workshop NUON Dome World Editor Tutorials

    Network Sponsors

    Apex Steel Pipe - Buys and sells Steel Pipe.
    Top