Декларативные подписки на события в Autofac

Недавно тут в качестве академической задачки попробовал запилить наивную реализацию декларативных подписок на события через IoC, в данном случае не используя брокер событий вообще. В отличии от моих старых постов про брокеры событий, в этот раз решил использовать не Unity а Autofac, который является нынче самым модным контейнером для .NET.

Для начала поясню в чем суть. Допустим у нас есть следующий набор интерфейсов:

public interface IEvent {}
public interface ISend<TEvent> where TEvent : IEvent
{
  event EventHandler<TEvent> Sender;
}
public interface IHandle<TEvent> where TEvent : IEvent
{
  void Handle(object sender, TEvent args);
}

IEvent — это «доменное событие». ISend<TEvent> реализует любая компонента, которая посылает сигнал типа TEvent, а IHandle<TEvent> реализует тот, кто это событие обрабатывает.

Вот допустим есть у нас кнопка которую кто-то может нажать. Представим что можно сделать, скажем, единичный или двойной щелчок по этой кнопке. Тогда можно это событие описать вот так:

public class ButtonPressedEvent : IEvent
{
  public int NumberOfClicks;
}

А саму кнопку, для простоты картины, вот как-то так:

public class Button : ISend<ButtonPressedEvent>
{
  public event EventHandler<ButtonPressedEvent> Sender;
  public void Fire(int clicks)
  {
    Sender?.Invoke(this, new ButtonPressedEvent
    {
      NumberOfClicks = clicks
    });
  }
}

Теперь представим что есть разные компоненты, которые хочется автоматически подписать на все события ISend<T> ежели они сами реализуют IHandles<T>. Вот пример такой компоненты:

public class Logging : IHandle<ButtonPressedEvent>
{
  public void Handle(object sender, ButtonPressedEvent args)
  {
    Console.WriteLine(
      $"Button clicked {args.NumberOfClicks} times");
  }
}

Итак: как использовать IoC контейнер для того чтобы автоматически подписать все хэндлеры на все возможные события в момент их создания?

Для начала, в контейнере мы регистрируем все генераторы событий:

var cb = new ContainerBuilder();
var ass = Assembly.GetExecutingAssembly();
// register publish interfaces
cb.RegisterAssemblyTypes(ass)
  .AsClosedTypesOf(typeof(ISend<>))
  .SingleInstance();

А теперь нужно зарегистрировать всех слушателей. Делается это как-то так:

cb.RegisterAssemblyTypes(ass)
  .Where(t =>
    t.GetInterfaces()
      .Any(i =>
        i.IsGenericType &&
        i.GetGenericTypeDefinition() == typeof(IHandle<>)))
  .OnActivated(act =>
  {
    var instanceType = act.Instance.GetType();
    var interfaces = instanceType.GetInterfaces();
    foreach (var i in interfaces)
    {
      if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IHandle<>))
      {
        var arg0 = i.GetGenericArguments()[0];
        var senderType = typeof(ISend<>).MakeGenericType(arg0);
        var allSenderTypes = typeof(IEnumerable<>).MakeGenericType(senderType);
        var allServices = act.Context.Resolve(allSenderTypes);
        foreach (var service in (IEnumerable) allServices)
        {
          var eventInfo = service.GetType().GetEvent("Sender");
          var handleMethod = instanceType.GetMethod("Handle");
          var handler = Delegate.CreateDelegate(
            eventInfo.EventHandlerType, null, handleMethod);
          eventInfo.AddEventHandler(service, handler);
        }
      }
    }
  })
  .SingleInstance()
  .AsSelf();

Немного жестоко, конечно, но оно работает. В коде выше, когда кто-то просит объект типа IHandler<T>, мы его создаем а также делаем поиск всех объектов типа ISend<T> и подписываемся на каждого из них. Если кто знает как упростить код выше — велкам!

Ну вот собственно и все, теперь вся эта штука работает:

var container = cb.Build();
var button = container.Resolve<Button>();
var logging = container.Resolve<Logging>();
button.Fire(2); // button clicked 2 times
button.Fire(4); // button clicked 4 times

Конечно, у этого подхода есть минусы, в частности:

  • Непонятно кто трекает всех созданных генераторов событий. У меня в коде читинг, а вот как на самом деле никто не знает, хотя какое-то решение я и сам использую для Динамического Прототипирования (хотя оно и тривиально).

  • Что будет если был создан новый паблишер — как автоматически подписать все существующие хэндлеры на него?

  • Если объект умирает, не происходит отписки от событий, а значит будут утечки памяти.

Так что для подписок лучше всего использовать IObservable<T>, хотя даже в этом случае декларативные подписки никто не отменял — просто с ними нужно быть осторожнее. ■

Оставить комментарий