Pull to refresh

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

Reading time7 min
Views7K


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


Задача


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

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

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

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

var p = new FootballPlayer { Name = "Arshavin" };<br/>
var c = new FootballCoach();<br/>
c.Watch(p);<br/>
p.Score(); // coach saw it!

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

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

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

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

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

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

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

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

Pub-sub


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

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

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

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

public class FootballPlayer<br/>
{<br/>
  private readonly EventBroker broker;<br/>
  public string Name { get; set; }<br/>
  public FootballPlayer(EventBroker broker)<br/>
  {<br/>
    this.broker = broker;<br/>
  }<br/>
  public void Injured()<br/>
  {<br/>
    broker.Publish("LeavingField"thisnew PlayerInjuredEventArgs());<br/>
  }<br/>
  public void SentOff()<br/>
  {<br/>
    broker.Publish("LeavingField"thisnew PlayerSentOffEventArgs());<br/>
  }<br/>
}<br/>

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

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

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

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

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

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

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

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

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


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

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

Articles