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

Вообще в сложных, динамических системах очень сложно угнаться за меняющейся организацией компонентов, и если мы еще кое-как (это в 21-м то веке!) разурлили проблему создания компонентов с помощью специализированных контейнеров, то взаимодействие из между собой нам все еще полностью не подвластно. Например, реагирование на события в .Net (да и в других языках наверное) сделано на каком-то уж очень несерьезном уровне. И естественным образом в этой задаче появляются всякие инфраструктурные решения, о которых мы и поговорим.

Задача

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

Как только с событиями нужно работать вплотную, игра тут же останавливается. Вот вам пример – берем футболиста и тренера:

public class FootballPlayer
{
  public string Name { get;set; }
  public void Score()
  {
    var e = Scored;
    if (e != null) e(this, new EventArgs());
  }
  public event EventHandler Scored; 
}
public class FootballCoach
{
  public void Watch(FootballPlayer p)
  {
    p.Scored += (_, __) =>
    {
      Console.WriteLine("I'm happy that {0} scored", p.Name);
    };
  }
}

В этом случае подписка и уведомление работают хорошо:

var p2 = c.Clone(); // deep copy :)
p.Score();

Весь прикол тут в том, что когда вы скопируете объект (будь то через MemberwiseClone() или deep copy с помощью BinaryFormatter), все подписки этого объекта будут потеряны.

Подписки можно, конечно, востановить руками или начать использовать вместо событий просто наборы делегатов или…. там, Expression<T>, что-то в этом роде. Но это только часть проблемы.

Следующая часть проблемы состоит в том, что в один прекрасный момент вы захотите чтобы ваши объекты подписывались на события автоматически. Например, вышел игрок на поле – тренер начинает за ним следить. С “отписками”, кстати, то же самое. Если все утрировать, получится примерно следующее:

class FootballCoach
{
  public FootballCoach(FootballPlayer[] players)
  {
    foreach (var p in players) {
      p.EntersField += new EventHandler(StartWatchingPlayer);
      p.LeavesField += new EventHandler(StopWatchingPlayer);
    }
  }
}

И так далее до посинения – в каждом StartXxx вы будете подписываться, в каждом EndXxx отписываться. Но и это еще не все.

Представьте теперь, что в системе таких объектов много. Все они посылают всем другим сообщения. Если делать пописки через +=, мы получим зверскую связанность и полную нетестируемость (а тестировать сообщения вообще сложно) нашего кода.

Ну и наконец надо и “о бедной конфигурации замолвить слово”. Ведь иногда хочется получать уведомления определенного типа вне зависимости от того, кто их послал. Например, судье толком все равно, кто на него матерится – игрок или тренер. (Это я так, символично.) А еще, не поверите, иногда хочется делать всякие хитрые преобразования вроде реагирования на события пакетами по 100 штук, раз в час, только в пятницу 13го в полнолуние, и т.п. К чему нынешнее положение дел не очень-то предрасположено.

Pub-sub

У среднестатистичного разработчика сразу появляется желание написать свой event broker. А что, почему бы и нет. Берем и пишем простой незамысловатый класс:

public class EventBroker
{
  private MultiDictionary<string, Delegate> subscriptions = 
    new MultiDictionary<string, Delegate>(true);
  public void Publish<T>(string name, object sender, T args)
  {
    foreach (var h in subscriptions[name])
      h.DynamicInvoke(sender, args);
  }
  public void Subscribe<T>(string name, Delegate handler)
  {
    subscriptions.Add(name, handler);
  }
}

Можно пошаманить над потокобезопасностью (я бы сюда инстинктивно воткнул ReaderWriterLockSlim) и т.п. но суть от этого не изменится. Получили мы брокер, который может заменеджить подписки на события. Конечно никакого QoS вы тут не получите, и всю логику связанную с выборками событий придется писать ручками, но уже есть некоторые подвижки – например, включив в качестве классификатора name, мы создали ситуацию в которой один класс может подписать обработчик на несколько эвентов одновременно.

Игрок больше не вывешивает события вообще.

public class FootballPlayer
{
  private readonly EventBroker broker;
  public string Name { get; set; }
  public FootballPlayer(EventBroker broker)
  {
    this.broker = broker;
  }
  public void Injured()
  {
    broker.Publish("LeavingField", this, new EventArgs());
  }
  public void SentOff()
  { // event args can be different for this one
    broker.Publish("LeavingField", this, new EventArgs());
  }
}

Теперь тренер подписывается через брокер:

public class FootballCoach
{
  private readonly EventBroker broker;
  public FootballCoach(EventBroker broker)
  {
    this.broker = broker;
  }
  public void Watch(FootballPlayer player)
  {
    broker.Subscribe<EventArgs>("LeavingField",
                                new EventHandler(PlayerIsLeavingField));
  }
  public void PlayerIsLeavingField(object sender, EventArgs args)
  {
    Console.WriteLine("Where are you going, {0}?",
                      (sender as FootballPlayer).Name);
  }
}

Здесь мы надеемся на полиморфность, связанную с тем что по канону все наследуют аргументы от EventArgs. Сильное типизирование тут не критично т.к. всегда можно делать приведение типов. Вот как все это будет выглядеть:

var uc = new UnityContainer();
uc.RegisterType<EventBroker>(
  new ContainerControlledLifetimeManager());
var p = uc.Resolve<FootballPlayer>();
p.Name = "Arshavin";
var c = uc.Resolve<FootballCoach>();
 
c.Watch(p);
 
p.Injured();
p.SentOff();

Что может сильно удивить так это то что события, фактически, были заменены на мессенджинг. Если вы как я много играетесь с NServiceBus и прочими системами, то это конечно вам покажется вполне естественной метаморфозой.

В примере выше, брокер зарегистрирован в контейнере как singleton, так что и игрок и тренер получат одну и ту же копию. Тут я на самом делаю не совсем тонкий намек на то, что в статических сценариях, когда известно кто на что подписывается, подписки можно описывать декларативно, т.е.:

public class FootballCoach
{
  [SubscribesTo("PlayerIsLeaving")]
  public void PlayerIsLeavingField(object sender, EventArgs args)
  {
    Console.WriteLine("Where are you going, {0}?",
                      (sender as FootballPlayer).Name);
  }
}

На то как это делается можно будет посмотреть в следующей части этого поста, а для тех кому не терпися, советую посмотреть сюда. Информация наверное немного устарела, но сам принцип думаю понятен.

Промежуточное заключение

Решение, приведенное здесь, не является конечным по ряду причин. Во-первых, как-то это некошерно иметь явную связь компонентов с брокером – получается что его нужно пробрасывать в каждый из классов, и в этих классах явно использовать. Эта проблема, как мы увидим в следующей части, вполне просто решается.

Вторая проблема, о которой мы кажется уже говорили, заключается в том, что описание какой-то логики в обработке событий сейчас ограничено одним критерием – строковым литералом который работает чем-то вроде “классификатора”. Ограничивать логику подобными методами – глупо, особенно в связи с присутствием такого мощного инструмента как LINQ. (Думаю, намек понят.)

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

  1. Спасибо, очень интересно, жду продолжения.

    Однако вот эта лямбда p.Scored += (_, __) =>
    повергла в ужас.

    Хорошо хоть не (_._)

    1. Это я из F# позаимствовал. На самом деле надо было написать (s, e) конечно.

  2. Только к середине статьи допёр о чём речь… А так, тема важная…
    И,насколько я понимаю, в дальнейшем будет затронута тема Rx ?

    1. Конечно будет!

  3. Спасибо, статья как всегда очень интересная.

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