Продолжая наше обсуждение брокеров событий, давайте посмотрим на то, как можно в статических сценариях (когда все подписки известны на момент компиляции) реализовать брокер через аттрибуты и расширение к используемому IoC-контейнеру. Внимание: это достаточно сложный пост на тему расширения Unity.
Выкладки брокера соответствуют аналогичным файлам из примеров в ObjectBuilder с той лишь поправкой что они переписаны для использования Unity 2.0. Примеры находятся тут: http://bitbucket.org/nesteruk/eventbrokers.
Собственно сам брокер
Начнем с простого, а именно с брокера. Реализация брокера подразумевает все те же два извечных участника – подписчик (event sink) и некто кто публикует события (event source). В реализации брокера для Unity, оба эти участника выделены в отдельные классы – это в какой-то мере отличается от нашей модели с Subscription
-классом из предыдущего поста. Почему? Все просто – там у нас была связка по интерфейсу. Тут у нас вообще полный loose coupling – мы только знаем что публикующий класс содержит событие (да-да, event
), а в “получающем” классе нам известен только метод который берет все те же (object, EventArgs)
к которым все уже привыкли. ОК, давайте посмотрим на эти инфраструктурные классы (а таких тут будет еще уйма).
EventSource
Этот класс инкапсулирует информацию об источнике события – именно поэтому в конструкторе появился EventInfo
. На основе информации о событии динамически формируется и добавляется подписка на это же событие. Также, по аналогии с нашим предыдущем примером, класс-источний помечен как IDisposable
, и его метод Dispose()
делает… угадайте что! Правильно – отписывает всех обработчиков от обработки этого события.
internal class EventSource : IDisposable { readonly string eventID; readonly EventInfo eventInfo; readonly MethodInfo handlerMethod; readonly EventBroker broker; readonly WeakReference source; public EventSource(EventBroker broker, object source, EventInfo eventInfo, string eventID) { this.broker = broker; this.source = new WeakReference(source); this.eventInfo = eventInfo; this.eventID = eventID; handlerMethod = GetType().GetMethod("SourceHandler"); Delegate @delegate = Delegate.CreateDelegate(eventInfo.EventHandlerType, this, handlerMethod); eventInfo.AddEventHandler(source, @delegate); } public object Source { get { return source.Target; } } public void Dispose() { object sourceObj = source.Target; if (sourceObj != null) { Delegate @delegate = Delegate.CreateDelegate(eventInfo.EventHandlerType, this, handlerMethod); eventInfo.RemoveEventHandler(sourceObj, @delegate); } } public void SourceHandler(object sender, EventArgs e) { broker.Fire(eventID, sender, e); } }
EventSink
Этот класс инкаспулирует в себе “приемник” событий. Он делает две вещи – во-первых, он как бы “регистрирует” привязку конкретного события к методу-обработчику – поэтому в конструкторе у нас фигурирует MethodInfo
. В конструкторе также формируется тип EventArgs
которые в последствии будут передаваться в обработчик.
Вторая вещь, которой занят приемник – это формирование и вызов того делегата, который отвечает за обработку конкретного события.
internal class EventSink { readonly Type handlerEventArgsType; readonly MethodInfo methodInfo; readonly WeakReference sink; public EventSink(object sink, MethodInfo methodInfo) { this.sink = new WeakReference(sink); this.methodInfo = methodInfo; ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length != 2 || !typeof(EventArgs).IsAssignableFrom(parameters[1].ParameterType)) throw new ArgumentException("Method does not appear to be a valid event handler", "methodInfo"); handlerEventArgsType = typeof(EventHandler<>).MakeGenericType(parameters[1].ParameterType); } public object Sink { get { return sink.Target; } } public Exception Invoke(object sender, EventArgs e) { object sinkObject = sink.Target; try { if (sinkObject != null) { Delegate @delegate = Delegate.CreateDelegate(handlerEventArgsType, sinkObject, methodInfo); @delegate.DynamicInvoke(sender, e); } return null; } catch (TargetInvocationException ex) { return ex.InnerException; } } }
Коротко о самом брокере
Кода получается много, поэтому попробую быстренько описать что у нас фигурирует в самом брокере. Естественно, что у нас есть списки подписчиков и обработчиков.
readonly ListDictionary<string, EventSink> sinks = new ListDictionary<string, EventSink>(); readonly ListDictionary<string, EventSource> sources = new ListDictionary<string, EventSource>();
Код использует класс ListDictionary
, который можно посмотреть в исходниках. В принципе, тут подошел бы и MultiDictionary
который мы использовали ранее.
Итак, вот какие методы реализует брокер:
RegisterSource()
,RegisterSink()
— добавление обработчиков и публикаторов (это валидное слово?) в спискиUnregisterSource()
,UnregisterSink()
— соответственно их удалениеFire()
— вызывает все обработчики для события с конкретным именемRemoveDeadSinksAndSources()
— удаляет из списков все “мертвые” ссылки; надеюсь понятно почему в исходниках либерально фигурируетWeakReference
Dispose()
— удаляет все источники
Коротко о расширении Unity
Наверное никого не удивит что контейнер Unity можно расширять, добавляя новый функционал. Чтобы сделать свое расширение, нужно просто отнаследовать от класса UnityContainerExtension
и дальше добавить те стратегии (о стратегиях поговорим через секунду) которые реализует данное расширение. Вот пример с нашим брокером:
public class BrokerExtension : UnityContainerExtension { private readonly EventBroker broker = new EventBroker(); protected override void Initialize() { Context.Container.RegisterInstance(broker, new ExternallyControlledLifetimeManager()); Context.Strategies.AddNew<BrokerReflectionStrategy>( UnityBuildStage.PreCreation); Context.Strategies.Add(new BrokerWireupStrategy(broker), UnityBuildStage.Initialization); } }
В данном примере, через переменную Context
мы имеем доступ к собственно контейнеру (в который мы добавляем сам брокер – а могли бы сделать и RegisterType<>
) а также к коллекции стратегий, которые будут применены. Для каждой стратегии описывается этап надстройки объекта, в котором эта стратегия задействована. У нас в примере две стратегии. Первая (BrokerReflectionStreategy
) для рефлешна, с помощью которого мы узнаем кто что публикует или хочет слушать.
public class BrokerReflectionStrategy : BuilderStrategy { public override void PreBuildUp(IBuilderContext context) { Type typeToBuild = context.BuildKey.Type; if (typeToBuild != null) { var policy = new EventBrokerPolicy(); RegisterSinks(policy, typeToBuild); RegisterSources(policy, typeToBuild); if (!policy.IsEmpty) context.Policies.Set<IEventBrokerPolicy>(policy, context.BuildKey); } base.PreBuildUp(context); } static void RegisterSinks(EventBrokerPolicy policy, Type type) { foreach (MethodInfo method in type.GetMethods()) foreach (SubscribesToAttribute attr in method.GetCustomAttributes(typeof(SubscribesToAttribute), true)) policy.AddSink(method, attr.Name); } static void RegisterSources(EventBrokerPolicy policy, Type type) { foreach (EventInfo @event in type.GetEvents()) foreach (PublishesAttribute attr in @event.GetCustomAttributes(typeof(PublishesAttribute), true)) policy.AddSource(@event, attr.Name); } }
Вторая – для того чтобы собственно зарегистрировать подписки (BrokerWireupStrategy
).
public class BrokerWireupStrategy : BuilderStrategy { private readonly EventBroker broker; public BrokerWireupStrategy(EventBroker broker) { this.broker = broker; } public override void PreBuildUp(IBuilderContext context) { var policy = context.Policies.Get<IEventBrokerPolicy>(context.BuildKey); if (policy != null && broker != null) { foreach (KeyValuePair<string, MethodInfo> kvp in policy.Sinks) broker.RegisterSink(context.Existing, kvp.Value, kvp.Key); foreach (KeyValuePair<string, EventInfo> kvp in policy.Sources) broker.RegisterSource(context.Existing, kvp.Value, kvp.Key); } base.PreBuildUp(context); } }
Тут происходит, казалось бы, странная вещь – используются две стратегии вместо одной. Более того, фигурирует также policy-класс под названием EventBrokerPolicy
который позволяет нам “перекинуть” данные из одной стратегии в другую. Одна из причин подобного – то, что в контексте стратегий не существует прямого выхода на контейнер. Иначе говоря, класс EventBrokerPolicy
ниже – это некий DTO.
public class EventBrokerPolicy : IEventBrokerPolicy { readonly Dictionary<string, MethodInfo> sinks = new Dictionary<string, MethodInfo>(); readonly Dictionary<string, EventInfo> sources = new Dictionary<string, EventInfo>(); public bool IsEmpty { get { return sinks.Count == 0 && sources.Count == 0; } } public IEnumerable<KeyValuePair<string, MethodInfo>> Sinks { get { return sinks; } } public IEnumerable<KeyValuePair<string, EventInfo>> Sources { get { return sources; } } public void AddSink(MethodInfo method, string eventID) { sinks.Add(eventID, method); } public void AddSource(EventInfo @event, string eventID) { sources.Add(eventID, @event); } }
Как все это выглядит
Итак, Unity находит подписки и публикации по аттрибутам Publishes
и SubscribesTo
. В нашем примере мы снова использовали строковый идентификатор для описания собственно событий – тем самым, у нас получается какой-то подобие loose coupling в том смысле что абслолютно произвольный компонент, воткнутый в систему, может подписаться на определенный тип события только в том случае, если знает как это событие называется.
Ну да ладно, посмотрим же как все это используется. Во-первых, наши классы FootballPlayer
и FootballCoach
из предыдущих примеров упростились до минимума. Вот как выглядит игрок:
public class FootballPlayer { [Publishes("score")] public event EventHandler PlayerScored; public string Name { get; set; } public void Score() { var ps = PlayerScored; if (ps != null) ps(this, new EventArgs()); } }
Естественно что в качестве eventArgs
мог бы быть более сложный тип – мы уже видели как наше расширение Unity само определяет этот тип и строит по нему делегат. Что касается тренера, теперь он выглядит вот так:
public class FootballCoach { [SubscribesTo("score")] public void PlayerScored(object sender, EventArgs args) { var p = sender as FootballPlayer; Console.Write("Well done, {0}!", p.Name); } }
Иначе говоря, подписка на события получилась у нас чисто декларативной. Вот собственно и все – можно пользоваться.
var uc = new UnityContainer(); uc.AddNewExtension<BrokerExtension>(); var p = uc.Resolve<FootballPlayer>(); p.Name = "Maradona"; var c = uc.Resolve<FootballCoach>(); p.Score(); p.Score();
Заключение
Декларативные подписки – это очень полезный функционал для конечного пользователя – ведь теперь можно аттрибутами помечать публикаторов событий и те методы, что их обрабатывают. В приведенном выше примере осталось несколько аспектов, которые можно улучшить – например, добавить функционала к аттрибутам и их обработке чтобы можно было выставлять сложные правила обработки событий. Также по-хорошему нужно было бы избавиться от класса ListDictionary
– это все-таки пережиток прошлого.
Получив декларативность мы потеряли ту экспрессивность что нам давали Reactive Extensions. К сожалению, совместить оба эти подхода достаточно сложно. Более того, мне кажется что они созданы для различных сценариев, и ничто не мешает подписчику самому сделать push-коллекцию из callback’ов и оперировать ей через Rx.
Вот собственно пока все. Comments welcome.
Оставить комментарий