Недавно тут в качестве академической задачки попробовал запилить наивную реализацию декларативных подписок на события через 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>
, хотя даже в этом случае декларативные подписки никто не отменял — просто с ними нужно быть осторожнее. ■
Оставить комментарий