Event Bus¶
Doelstelling¶
Ik wou de Single Responsibility Principle van SOLID(SOLID, z.d.) nog beter toepassen door door middel van events(Event-driven architecture, z.d.) functionaliteit nog meer los te koppelen van de reactie. Zelfs met de mogelijkheid om nu op meerdere plekken functionaliteit te hebben dat reageert op één dezelfde event.
Toegepaste oplossing¶
EventBus¶
Hiervoor heb ik de event bus gemaakt, wat heel losjes is gebaseerd op het event systeem van Minecraft(FORGE docs, Events, z.d.). Het is namelijk veel eenvoudiger. Of in ieder geval het deel ervan dat beschikbaar is in de Forge modloader, want de source code van Minecraft zelf heeft niet een mooie publieke documentatie pagina.
Ik gebruik hier net zoals met het systemen systeem een queue om de methods alleen op een voorspelbare moment op te roepen.
De listeners op de events worden hier opgeslagen als delegates,
omdat het niet mogelijk is in C# om een type te casten als de generic niet precies hetzelfde is.
In dit geval Action<T : struct>
naar Action<object>
.
Terwijl de type in de generic wel gecast kan worden.
public class EventBus : BaseSystem
{
protected Dictionary<Type, List<Delegate>> events = new();
protected Queue<object> eventQueue = new();
public void Publish<T>(T data) where T : struct
=> eventQueue.Enqueue(data);
public bool Unsubscribe<T>(Action<T> listener) where T : struct
=> Unsubscribe(typeof(T), listener);
public bool Unsubscribe(Type eventType, Delegate listener)
{
var hasEventType = events.TryGetValue(eventType, out var listenersList);
if (!hasEventType || listenersList == null)
{
App.Logger.LogWarning("Tried disposing '{}' event listener, but the event has no listeners", eventType.Name);
return false;
}
var removed = listenersList.Remove(listener);
if (!removed)
{
App.Logger.LogWarning("Tried disposing '{}' event listener, but the listener could not be found", eventType.Name);
}
return removed;
}
Hier heb ik een speciale class gemaakt, alleen maar om IDisposable te implementeren. Daarom is het privé en sealed. Deze is er zodat je op dezelfde manier als met het vorige project listeners kan verwijderen. Simpel door het de disposen.
Maar in het geval dat kan het ook op een meer directe manier met de methods hierboven, die dan ook door deze class worden gebruikt.
private sealed class EventListenerDisposer(EventBus bus, Type type, Delegate listener) : IDisposable
{
private readonly EventBus bus = bus;
private readonly Type eventType = type;
private readonly Delegate listener = listener;
public void Dispose()
=> bus.Unsubscribe(eventType, listener);
}
public IDisposable Subscribe<T>(Action<T> listener) where T : struct
{
var gotListeners = events.TryGetValue(typeof(T), out var listeners);
listeners ??= new();
listeners.Add(listener);
if (!gotListeners)
{
events.Add(typeof(T), listeners);
}
return new EventListenerDisposer(this, typeof(T), listener);
}
Als laatste word hier ervoor gezorgd dat de listeners op events worden opgeroepen. Dit word ook gelijk gedaan met het systemen systeem als goed voorbeeld voor de rest van het project.
public void FlushEvents()
{
foreach (var e in eventQueue)
{
var eventType = e.GetType();
if (!events.ContainsKey(eventType))
{
App.Logger.LogTrace("Sent '{}' event, but it has no listeners", eventType.Name);
continue;
}
using var scope = App.Logger.BeginScope("Event '{}'", eventType.Name);
foreach (var listener in events[eventType])
listener.DynamicInvoke(e);
}
eventQueue.Clear();
}
public override void HandleInput(InputHelper inputHelper)
=> FlushEvents();
}
Voorbeelden¶
Eigenlijk is er nog nergens dat events al gebruikt kunnen worden, dus kon ik alleen een paar voorbeelden geven. Wel laten ze mooi zien hoe simpel events zijn. En toevallig ook hoe makkelijk het systemen systeem is om te gebruiken.
public struct StateChangedEvent(BaseState? prevState, BaseState? newState)
{
public readonly BaseState? previousState = prevState;
public readonly BaseState? newState = newState;
}
Deze word hier in de StateManager gepublished.
private T SwitchToState<T>(T state) where T : BaseState?
{
systemHandler.GetSystem<EventBus>()
.Publish(new StateChangedEvent(currentState, state));
...
}
Bronnen¶
- C# documentation. (z.d.) learn.microsoft.com.
Laatst geraadpleegd op 18 april 2024, van https://learn.microsoft.com/en-uk/dotnet/csharp/ - SOLID. (z.d.) wikipedia.
Laatst geraadpleegd op 18 april 2024, van https://en.wikipedia.org/wiki/SOLID - Event-driven architecture. (z.d.) wikipedia.
Laatst geraadpleegd op 18 april 2024, van https://en.wikipedia.org/wiki/Event-driven_architecture - FORGE docs, Events. (z.d.) docs.minecraftforge.net.
Laatst geraadpleegd op 16 april 2024, van https://docs.minecraftforge.net/en/1.20.x/concepts/events/
Gecreëerd: April 19, 2024