3

I'm working on a small hobby project with someone as a learning exercise. In it, the user traverses a dungeon, finding treasure, fighting monsters, etc. etc.

Right now, the approach looks a little bit like this (pseudocode):

public class DungeonRunner
{
    public ICombatEngine CombatEngine;
    public ITreasureEngine TreasureEngine;
    public ITrapEngine TrapEngine;

    //ctor injections etc. etc.

    public void RunDungeon(List<IEvent> dungeonEvents)
    {
        foreach(var event in dungeonEvents)
        {
            // my issue starts here
            switch(event)
            {
                case event is CombatEvent:
                    CombatEngine.Run(event as CombatEvent);
                case event is TreasureEvent:
                    TreasureEngine.Run(event as TreasureEvent);
                //etc etc
            }
        }
    }
}

This works OK, but looks to be a violation of the open-closed principle.

Ideally, I'd have something that looks a bit like this:

public void RunDungeon(List<IEvent> dungeonEvents)
{
    foreach(var event in dungeonEvents)
    {
        event.Run();
    }
}

But there are issues here. Right now, all our IEvent-implementing classes are essentially just data that describes how something runs (which monster you fight, what treasure you find, etc), and the Engine classes process that into actual game-things happening.

To do it with the above example, the Events themselves would need to have a dependency on their pertinent engine, which definitely doesn't feel right.

Can anyone suggest a better approach? We really don't want an endlessly growing switch statement.

13
  • A dictionary that maps event type to the execution engine implementing Run for that type? Commented Sep 9, 2024 at 14:26
  • what's the volumes here? would passing every event to every engine work, having each engine figure out what it wants to care about? if you have an abstract class EngineBase<T> where T : IEvent, the base type can have void IEngine.Run(IEvent evt) { if (evt is T typed) { Run(typed); } } protected abstract void Run(T evt); etc Commented Sep 9, 2024 at 14:33
  • @Ralf I like the dictionary idea as it feels a little bit cleaner, though it is still a violation of the open-closed principle; if you add a new type of IEvent, you must add an entry to the dictionary. Commented Sep 9, 2024 at 14:35
  • 1
    @DavidOsborne Unfortunately, while it's what I'd do in the ordinary case, the issue is also one of dispatch; unless the events carry their own dependencies, this won't work. Commented Sep 11, 2024 at 8:08
  • 1
    @ConnieMnemonic You can get rid of the switch if you re-write the handler slightly. Inject the IServiceProvider into your DungeonRunner. Then in your RunDungeon you loop through the events, and use something similar to: handler = _serviceProvider.GetService<IEventHandler<event.GetType()>>(); followed by handler.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. Commented Sep 23, 2024 at 9:42

3 Answers 3

0

Your construct dos not violate the Open-Closed Principle (Wikipedia.)

The Open-Closed Principle says that "software entities should be open for extension, but closed for modification".

  • The principle does not say you must extend classes, it just says that classes must be open for extension, but this does not mean that you have to extend them; you can go ahead and not extend them; the principle has nothing to say about that.

  • Your construct does not involve any modification to any event classes.

Also, the open-closed principle is rather obsolete today: we often mark classes as sealed precisely to prevent anyone from extending them. A more pertinent mantra today is Joshua Bloch's "Design and document for inheritance—or else prohibit it."

Source: Oracle: Java Magazine: Blogs: "Design and document for inheritance—or else prohibit it. Here’s how" by Joshua Blosh.

Not only is your construct fine, but if you are using C# 7 and above, the language supports special syntax called "Pattern Matching" in switch statements to facilitate doing precisely that:

        switch( event )
        {
            case CombatEvent combatEvent:
                CombatEngine.Run( combatEvent );
                break;
            case TreasureEvent treasureEvent:
                TreasureEngine.Run( treasureEvent );
                break;
            //etc etc
        }

See Microsoft Learn: "Dissecting the pattern matching in C# 7" by Sergey Tepliakov

And if you are using C# 9 and above there are even more features facilitating the same thing in a a more functional style.

See Microsoft Learn: "Pattern matching - the is and switch expressions, and operators and, or and not in patterns"

We do switch statements like that in C# quite a lot. It helps keep the data separate from the logic.

These switch statements may grow long, and this can sometimes be a bit of a problem, but it is not prohibitive. The benefits of doing it this way outweigh the disadvantages. And unlike the Java world, in the C# world people are not afraid of large source files containing big classes with long methods. Methods should, ideally, be kept small, but there is no inviolable rule which says that they must be small; if you have a real reason to have a long method, then by all means, have one. Or even a few. It is not a problem.

Sign up to request clarification or add additional context in comments.

1 Comment

Interesting, thank you. Can I ask, is it not seen as a problem that the switch will grow larger and larger as we add more events?
0

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>();

Comments

0

The simplest solution is to keep the switch logic as is as long as the number of cases is reasonable. Why? The logic is encapsulated in one place. It fits in your head and doesn't bleed into other code.

If you're concerned about cases ballooning out of control, then use a dictionary and dynamically invoke the Run() on the engine.

var engines = new Dictionary<IEvent, IEngine<IEvent>();
engines[typeof(CombatEvent] = new CombatEngine(); // where CombatEngine : IEngine<CombatEvent>
...
engines[event.GetType()]).Run((dynamic)event);

You can even combine this. Keep the most common cases in the switch logic, and farm off the rest to the dictionary -- knowing there's some overhead to dynamic of course.

switch( event )
{
    case CombatEvent combatEvent:
        CombatEngine.Run( combatEvent );
        break;
        ...
    default:
        engines[event.GetType()]).Run((dynamic)event);
}

None of this requires reflection other than the GetType() though there are late-bound concerns with dynamic. If there are other dependencies, like engines requiring other engines or other stateful information, you will need to introduce some sort of context object that can be passed to Run().

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.