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