Как написать EventManager/Observer на много событий без Reflection?

Допустим, у нас есть Observer и много-много разных событий (сотни). Стандартный Observer, описанный в википедии предлагает их различать по строковому идентификатору. Можно вспомнить, что мы хотим пользоваться всеми возможностями IDE и ввести какой-нибудь Enum вместо строкового идентификатора, но я заметил, что в результате метод Update раздувается до приблизительно такого свича:
void Update (Event e) {
  switch (e.type) {
    case EventType.Move: 
      OnMove( (MoveEvent) e );
      break;
    // ...
    case EventType.Build: 
      OnBuild( (BuildEvent) e );
      break;
  }
}


В результате метод раздувается до офигенно большого свитча, который только и делает, что вызывает другие методы. Хотелось бы более изящного решения. В Юнити такое реализовано через Reflection (OnUpdate и т.п.), но мне кажется это не очень изящным.

Допустим, у нас есть такой EventManager:

class EventManager {
    List<IListener> listeners = new List<IListener>();
    
    public void Subscribe (IListener listener) {
        listeners.Add( listener );
    }
    
    public void Publish (Event e) {
        foreach (IListener listener in listeners) {
            listener.OnEvent( e );
        }
    }
}

class Event {
    public virtual string GetTitle () {
        return "Title:Event";
    }
}

interface IListener {
    void OnEvent (Event e);
}


И много-много (на каждый чих) потенциальных событий. Скажем, несколько сотен:

class BuildEvent : Event {
    public override string GetTitle () {
        return "Title:BuildEvent";
    }
}

class MoveEvent : Event {
    public override string GetTitle () {
        return "Title:MoveEvent";
    }
}

class AttackEvent : Event {
    public override string GetTitle () {
        return "Title:AttackEvent";
    }
}


class Program {
    static void Main() {
        Console.WriteLine("Hello, World!");
        
        EventManager manager = new EventManager();

        // Хендлеров может быть много и разным хендлерам нужны разные события
        // Скажем, меню построек не реагирует на базовые действия юнитов
        // А модулю статистики не важны факты об окончании ремонта
        manager.Subscribe( new GameHandler() );
        manager.Subscribe( new MenuHandler() );
        manager.Subscribe( new CostHandler() );
        manager.Subscribe( new LogHandler() );

        // ну и тут эти события постоянно запускаются, допустим, это длительная сессия игры
        manager.Publish( new BuildEvent() );
        manager.Publish( new MoveEvent() );
        manager.Publish( new BuildEvent() );
        manager.Publish( new BuildEvent() );
        manager.Publish( new AttackEvent() );
    }
}


Так вот, как бы написать эту либу, чтобы можно было доподписывать нужные события? В идеале было бы, например, если это будет реализовано через method override
class GameHandler : IListener {
    // Все события, которые зашли сюда - игнорируются
    public void OnEvent (Event e) {}
    
    // Необходимые события переопределяю отдельными методами через аргумент функции
    public void OnEvent (MoveEvent e) {
        Console.WriteLine( "Foo.OnMoveEvent: " + e.GetTitle() );
    }
    
    public void OnEvent (BuildEvent e) {
        Console.WriteLine( "Foo.OnBuildEvent: " + e.GetTitle() );
    }
}


Естественно, в подобной реализации идея не сработает. Такое, конечно, можно сделать свичем в OnEvent (Event e), как в первом блоке кода, но не хотелось бы писать глупый код и дублировать функциональность языка. Допускаю, что реализовывается через рефлексию, но хотелось бы ее избежать. Какие практики обычно используются в Java и C#?
  • Вопрос задан
  • 442 просмотра
Пригласить эксперта
Ответы на вопрос 2
@Hydro
C#/.NET Developer
В C# уже есть механизм событий, прячущий под капот паттерн Observer.
См. ключевое слово event.
А вообще switch в методе Update можно заменить на паттерн ServiceLocator, по сути
public class EventManager
{
  Dictionary<EventType, EventHandler> handlerLocator.
  public void RegisterEventHandler(EventType type, EventHandler handler)
  {
    this.handlerLocator[type] = handler;
  }

  public void Update(Event e)
  {
    var handler = this.handlerLocator[e.Type];
    handler(this, e.EventArgs);
  }
}
Ответ написан
dordzhiev
@dordzhiev
Можно написать абстрактный класс, в котором будут пустые реализации OnEvent для каждого события, а в наследниках уже их переопределять при необходимости. Например так:
class EventManager
{
    List<EventListenerBase> listeners = new List<EventListenerBase>();

    public void Subscribe(EventListenerBase listener)
    {
        listeners.Add(listener);
    }

    public void Publish(Event e)
    {
        foreach (EventListenerBase listener in listeners)
        {
            listener.OnEvent(e);
        }
    }

    public void Publish(MoveEvent e)
    {
        foreach (EventListenerBase listener in listeners)
        {
            listener.OnEvent(e);
        }
    }

    public void Publish(BuildEvent e)
    {
        foreach (EventListenerBase listener in listeners)
        {
            listener.OnEvent(e);
        }
    }
}

abstract class EventListenerBase
{
    public virtual void OnEvent(Event e)
    {
            
    }

    public virtual void OnEvent(MoveEvent a)
    {
            
    }

    public virtual void OnEvent(BuildEvent a)
    {
            
    }
}

class GameHandler : EventListenerBase
{
    public override void OnEvent(Event e)
    {
        Debug.WriteLine("Event");
    }

    public override void OnEvent(BuildEvent a)
    {
        Debug.WriteLine("BuildEvent");
    }

    public override void OnEvent(MoveEvent a)
    {
        Debug.WriteLine("MoveEvent");
    }
}

Но этот способ отвратителен, никогда его не используйте :) Хотя бы потому что для каждого события нужно делать отдельный метод OnEvent и Publish, и работает это только в статике, компилятор выбирает нужный метод во время компиляции, т.е. manager.Publish((Event) new MoveEvent()) не вызовет обработчик для MoveEvent.

Такие вещи (и в Java и в C#) лучше делать рефлексией или с помощью Expression в C#. О производительности Reflection можно не беспокоиться, т.к. регистрировать обработчики достаточно все один раз.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы
Hamilton Apps Москва
от 150 000 до 300 000 ₽
ЭЛКОМ+ Москва
от 70 000 до 165 000 ₽
deeplay Новосибирск
от 130 000 до 200 000 ₽
17 янв. 2021, в 01:26
100000 руб./за проект
16 янв. 2021, в 22:34
10000 руб./за проект
16 янв. 2021, в 22:33
20000 руб./за проект