Брокеры событий, часть 3

Продолжая наше обсуждение брокеров событий, давайте посмотрим на то, как можно в статических сценариях (когда все подписки известны на момент компиляции) реализовать брокер через аттрибуты и расширение к используемому 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.

7 responses to “Брокеры событий, часть 3”

  1. Мне кажется, Вы достаточно простую задачу решаете слишком сложными методами.

    1. Предложите альтернативу :)

  2. Вячеслав Avatar
    Вячеслав

    Увы мы декларативного программирования хлебнули через край.
    Нет ничего тяжелее, чем поиск в огромном проекте декларативных связей!

    1. А вы не рассматривали варианты создания тулзов для этого? Мне на ум сразу приходят визуализаторы IoC-контейнеров и аналогичные вещи.

  3. Вячеслав Avatar
    Вячеслав

    Интересная идея, но тулза будет работать ровно до того момента пока знаешь, что и где искать.

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