Based on our conversation in the comment section here is a sample solution to avoid a growing switch statement.
There is most likely many better ways to implement this but I'm familiar with this one so I went with that.
The tradeoff is that you are taking a performance hit instead. How much, I don't know.
The choice here is between a growing switch, requiring updating the switch as events are added, and using generics adding event handlers as events are added.
Either solution will be clean code, maintainable and testable.
My2 cents. While very strictly, an ever growing switch could be seen to
break OCP and could end up scary to maintain, it is rarely a reason to
trade in for a generic solution. Personally I never seen massive
switch statements that made me run for the hills. Even if there is 10 or 15 events, thats fine in
most cases as long as its clean and logical code is abstracted, such
as in your engines with their own tests.
DungeonRunner
I made a small web app and updated the DungeonRunner as follows:
public class DungeonRunner : IDungeonRunner
{
private readonly IServiceProvider _serviceProvider;
public DungeonRunner(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void RunDungeon(List<IEvent> dungeonEvents)
{
foreach (var dungeonEvent in dungeonEvents)
{
var eventType = dungeonEvent.GetType();
var handlerType = typeof(IEventHandler<>).MakeGenericType(eventType);
var handler = _serviceProvider.GetService(handlerType);
handlerType.GetMethod("Run")?.Invoke(handler, null);
}
}
}
You can ignore the use of IDungeonRunner, that was just for me so I
can inject it into my controller for testing.
This will loop through the events as before but instead of an action per event type in a switch statement we make use of reflection, using MakeGenericType to allow us dynamically to select the correct handler type/signature i.e: IEventHandler<EventType>
Using the ServiceProvider we can then retrieve the correct implementation for the expected type.
As we can't cast the handler to it's concrete type we need to use reflection again to invoke the Run method in the event handler.
Event Handlers
All event handlers will implement this interface:
public interface IEventHandler<TEvent> where TEvent: IEvent
{
public void Run();
}
The TEvent is not actually used but is required to ensure we can use generics to identify the correct handler implementation by event type without using a switch in the DungeonRunner.
The implementations tie the engine to the event type and may look as follows:
public class CombatEventHandler : IEventHandler<CombatEvent>
{
private readonly ICombatEngine _engine;
public CombatEventHandler(ICombatEngine engine)
{
_engine = engine;
}
public void Run()
{
_engine.Run();
}
}
public class TreasureEventHandler : IEventHandler<TreasureEvent>
{
private readonly ITreasureEngine _engine;
public TreasureEventHandler(ITreasureEngine engine)
{
_engine = engine;
}
public void Run()
{
_engine.Run();
}
}
Testing
To test if the correct engines execute I added an output to the run methods:
public class CombatEngine : ICombatEngine
{
public void Run()
{
Console.WriteLine("Combat Engine: Run was executed");
}
}
public class TreasureEngine : ITreasureEngine
{
public void Run()
{
Console.WriteLine("Treasure Engine: Run was executed");
}
}
As I used a web core application I added a simple test controller:
public class DungeonController : ControllerBase
{
private readonly IDungeonRunner _runner;
public DungeonController(IDungeonRunner runner)
{
_runner = runner;
}
[HttpGet]
public async Task Get()
{
var dungeonEvents = new List<IEvent>
{
new CombatEvent(),
new TreasureEvent()
};
_runner.RunDungeon(dungeonEvents);
}
}
I could have used a console app but was already working in a web core
app on a similar test project. Also, ingnore the GET not returning
aything, I just threw an action in to quickly test the code.
I executed the controller action using this .http file:
GET {{EventHandler_HostAddress}}/Dungeon/
Accept: application/json
The output did print correctly in the console window in the correct order.
Service Registration
For completness here is the service registration:
builder.Services.AddScoped<IDungeonRunner, DungeonRunner>();
builder.Services.AddScoped<IEvent, CombatEvent>();
builder.Services.AddScoped<IEvent, TreasureEvent>();
builder.Services.AddScoped<ICombatEngine, CombatEngine>();
builder.Services.AddScoped<ITreasureEngine, TreasureEngine>();
builder.Services.AddScoped<IEventHandler<CombatEvent>, CombatEventHandler>();
builder.Services.AddScoped<IEventHandler<TreasureEvent>, TreasureEventHandler>();
abstract class EngineBase<T> where T : IEvent, the base type can havevoid IEngine.Run(IEvent evt) { if (evt is T typed) { Run(typed); } } protected abstract void Run(T evt);etcIEvent, you must add an entry to the dictionary.IServiceProviderinto yourDungeonRunner. Then in yourRunDungeonyou loop through the events, and use something similar to:handler = _serviceProvider.GetService<IEventHandler<event.GetType()>>();followed byhandler.Run()The concrete Handler then knows about which engine to run for the given event. I see if I can get a small console app to work as an example locally to check if the suggested code would actually work.