Задать вопрос

Как написать 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#?
  • Вопрос задан
  • 557 просмотров
Подписаться 2 Оценить Комментировать
Пригласить эксперта
Ответы на вопрос 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 можно не беспокоиться, т.к. регистрировать обработчики достаточно все один раз.
Ответ написан
Комментировать
Ваш ответ на вопрос

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

Похожие вопросы