⬆️ ⬇️

Simple event system - non-standard approach

This note is a logical continuation of the post "SMessage - A Simple and Predictable Event System for Unity" . The user erlioniel described two approaches to building his own message system: using enumerations and message classes. In my note, I want to talk about a possible way of mixing these approaches and about the bicycle that can come of it.



We cross with the hedgehog - the third approach



Obviously, static typing allows you to weed out a large number of errors and helps in writing code. But the implementation through enum has its advantage - instead of creating a new message object, the parameters are directly passed to the handler function.



Is it possible to get rid of instantiating message objects, but not losing all the advantages of static typing?

')

Yes you can. To do this, we formulate in more detail what we want to get. We need a result similar to the method of sending a message via enum. That is, the method of the form:

events.Dispatch(  ,   ); 


At the same time, the “event identifier” should unambiguously determine the “event parameters”.



We will come to the aid of generic'i. The first signature that comes to mind is as follows:

 public void Dispatch<TEvent, TSender, TArg>(TEvent e, TSender sender, TArg arg) 


Obviously, it is no better than implementation through enums, since TEvent , TSender and TArg independent. But we know that an event must unambiguously determine its parameters. We describe it as follows:

 public void Dispatch<TEvent, TSender, TArg>(IEvent<TEvent, TSender, TArg> e, TSender sender, TArg arg) where TEvent : IEvent<TEvent, TSender, TArg> 


First, note that the first argument describes all the generic types of the method. Secondly, we have an unambiguous relationship between the event and its parameters. Now the IDE will be able to determine the types of the remaining parameters by the first argument.



What is IEvent ? Let us consider in more detail the limitations of type parameters (the where construct). It can be seen that the type of TEvent recursively defined through itself. Therefore, IEvent define IEvent as well:

 public abstract class IEvent<TEvent, TSender, TArg> where TEvent : IEvent<TEvent, TSender, TArg> { } 


Great, now we can announce our event along with the parameter types:

 public class StartTurnEvent : IEvent<StartTurnEvent, Player, int> { } 




Let's return to the question of sending a message. Now it looks like this:

 events.Dispatch(new StartTurnEvent(), player, 123); 


We came up with a syntax similar to enums, but instantiation has not gone away.

On the other hand, it is obvious that we are not interested in the object itself, but in its type. Verify this by looking inside the Dispatch method:

 public void Dispatch<TEvent, TSender, TArg>(IEvent<TEvent, TSender, TArg> e, TSender sender, TArg arg) where TEvent : IEvent<TEvent, TSender, TArg> { foreach (var action in handlers.Get<Action<TSender, TArg>>(typeof(TEvent))) { action(sender, arg); } } 


You can see that the first argument is not used at all, that is, you can call Dispatch without instantiating:

 events.Dispatch((StartTurnEvent)null, player, 123); 


The final touch will get rid of the extra brackets and the word null . We put it into a constant:

 public abstract class IEvent<TEvent, TSender, TArg> where TEvent : IEvent<TEvent, TSender, TArg> { public readonly static TEvent Tag = null; } 




What did we get in the end?

1. The syntax is similar to sending messages through enums.

2. Static typing and IDE hints.

3. We got rid of instantiating messages before each sending.



Comparison of approaches

Event Announcement:

 //  0:    Unity3D /*     */ //  1:   enum' enum Events { StartTurn } //  2:    public class StartTurnMessage : IMessage<Player, int> { public StartTurnMessage(Player player, int value) : base(player, value) { } } //  3:  public class StartTurnEvent : IEvent<StartTurnEvent, Player, int> { } 


Posting:

 //  0:    Unity3D player.SendMessage("OnStartTurn", 123); //    IDE //  1:   enum' events.Dispatch(Events.StartTurn, player, 123); //     //  2:    events.Dispatch(new StartTurnMessage(player, 123)); //  3:  events.Dispatch(StartTurnEvent.Tag, player, 123); 


Source
 using System; using System.Collections.Generic; using System.Linq; using Collection = System.Collections.Generic.HashSet<object>; namespace SteamSquad.Gameplay { public class EventBus { private readonly Container handlers = new Container(); public void Unsubscribe<TEvent, TSender>(IEvent<TEvent, TSender> tag, Action<TSender> action) where TEvent : IEvent<TEvent, TSender> { handlers.Remove(typeof(TEvent), action); } public void Unsubscribe<TEvent, TSender, TArg>(IEvent<TEvent, TSender, TArg> tag, Action<TSender, TArg> action) where TEvent : IEvent<TEvent, TSender, TArg> { handlers.Remove(typeof(TEvent), action); } public void Subscribe<TEvent, TSender>(IEvent<TEvent, TSender> tag, Action<TSender> action) where TEvent : IEvent<TEvent, TSender> { handlers.Add(typeof(TEvent), action); } public void Subscribe<TEvent, TSender, TArg>(IEvent<TEvent, TSender, TArg> tag, Action<TSender, TArg> action) where TEvent : IEvent<TEvent, TSender, TArg> { handlers.Add(typeof(TEvent), action); } public void Dispatch<TEvent, TSender>(IEvent<TEvent, TSender> tag, TSender sender) where TEvent : IEvent<TEvent, TSender> { foreach (var action in handlers.Get<Action<TSender>>(typeof(TEvent))) { action(sender); } } public void Dispatch<TEvent, TSender, TArg>(IEvent<TEvent, TSender, TArg> tag, TSender sender, TArg arg) where TEvent : IEvent<TEvent, TSender, TArg> { foreach (var action in handlers.Get<Action<TSender, TArg>>(typeof(TEvent))) { action(sender, arg); } } public abstract class IEvent<TEvent, TSender> where TEvent : IEvent<TEvent, TSender> { public readonly static TEvent Tag = null; } public abstract class IEvent<TEvent, TSender, TArg> where TEvent : IEvent<TEvent, TSender, TArg> { public readonly static TEvent Tag = null; } private class Container { private readonly Dictionary<Type, Collection> container = new Dictionary<Type, Collection>(); public void Add(Type type, object action) { Collection collection; if (!container.TryGetValue(type, out collection)) { container.Add(type, collection = new Collection()); } collection.Add(action); } public void Remove(Type type, object action) { container[type].Remove(action); } public IEnumerable<TAction> Get<TAction>(Type type) { Collection collection; if (container.TryGetValue(type, out collection)) { return collection.OfType<TAction>(); } return Enumerable.Empty<TAction>(); } } } } 

Source: https://habr.com/ru/post/245487/



All Articles