EventAggregator — антипаттерн
EventAggregator можно найти во многих WPF-каркасах: Mvvm Light -класс Messenger, Catel — класс MessageMediator. Я познакомился с EventAggregator вместе с WPF каркасом Prism. Использование EventAggregator оказалось простым и гибким. Компоненты системы становятся независимыми друг от друга — изменяя один компонент, я не боюсь сломать другой.
При рассмотрении отдельных компонент все так и есть, но поднявшись на уровень работы компонентов в системе, можно разглядеть серьёзные проблемы:
Делюсь моим взглядом на слишком слабую связанность и не явное взаимодействие между частями системы.
Управление светодиодами через EventAggregator
Для управления светодиодами понадобятся: кнопка питания — Power, переключатель с двумя состояниями — Switch и два светодиода — RedLed и BlueLed. На WPF это выглядит, как то так:
Кнопка Power зажигает один из светодиодов в зависимости от состояния переключателя Switch.
В системе основанной на EventAggregator выделим два события: включения/выключения питания — PowerEvent и изменение состояния переключателя — SwitchEvent.
Событие PowerEvent публикуется при нажатии на кнопку Power, событие SwitchEvent публикуется при нажатии на Switch. Светодиоды подписываются на события PowerEvent и SwitchEvent.
Светодиод зажигается, если есть питание и переключатель находится в нужном состоянии.
enum Power
{
On = 1,
Off = 0,
}
class PowerEvent : PubSubEvent
{
}
public enum SwitchConnection
{
Connection1,
Connection2,
}
class SwitchEvent : PubSubEvent
{
}
public class PowerViewModel : BindableBase
{
readonly IEventAggregator _aggregator;
bool _power;
public PowerViewModel(IEventAggregator aggregator)
{
_aggregator = aggregator;
}
public bool Power
{
get { return _power; }
set
{
if (SetProperty(ref _power, value))
_aggregator.GetEvent().Publish(_power ? Events.Power.On : Events.Power.Off);
}
}
}
public class SwitchViewModel : BindableBase
{
readonly IEventAggregator _aggregator;
bool _switch;
public SwitchViewModel(IEventAggregator aggregator)
{
_aggregator = aggregator;
Switch = true;
}
public bool Switch
{
get { return _switch; }
set
{
if (SetProperty(ref _switch, value))
_aggregator.GetEvent().Publish(_switch ? SwitchConnection.Connection1 : SwitchConnection.Connection2);
}
}
}
///
/// ViewModel светодиода.
///
public class LedViewModel : BindableBase
{
readonly SwitchConnection _activeConnection;
readonly Brush _activeLight;
Power _currentPower;
SwitchConnection _currentConnection;
Brush _currentlight;
public LedViewModel(SwitchConnection connection, Brush light, IEventAggregator aggregator)
{
_activeConnection = connection;
_activeLight = light;
aggregator.GetEvent().Subscribe(OnPowerChanged);
aggregator.GetEvent().Subscribe(OnSwitch);
Update();
}
///
/// Свет от светодиода.
///
public Brush Light
{
get { return _currentlight; }
private set
{
SetProperty(ref _currentlight, value);
}
}
///
/// Обработчик переключателя.
///
void OnSwitch(SwitchConnection connection)
{
if (SetProperty(ref _currentConnection, connection))
Update();
}
///
/// Обработчик питания.
///
void OnPowerChanged(Power power)
{
if (SetProperty(ref _currentPower, power))
Update();
}
void Update()
{
Brush currentLight = Brushes.Transparent;
switch (_currentPower)
{
case Power.On:
if (_currentConnection == _activeConnection)
currentLight = _activeLight;
break;
case Power.Off:
break;
default:
throw new ArgumentOutOfRangeException();
}
Light = currentLight;
}
}
var aggregator = new EventAggregator();
PowerVM = new PowerViewModel(aggregator);
SwitchVM = new SwitchViewModel(aggregator);
Connection1Light = new LedViewModel(SwitchConnection.Connection1, Brushes.Red, aggregator);
Connection2Light = new LedViewModel(SwitchConnection.Connection2, Brushes.Blue, aggregator);
Все работает отлично!
Проблемы с EventAggregator
В схеме всего 4 компонента, но что нужно сделать, что бы светодиод заменить на другой элемент? Скопипастить подписку из LedViewModel — продублировать.
При динамической замене компонент, все еще хуже, везде нужно будет дублировать отписку. EventAggregator по умолчанию создает weakreference. С Weakreference отписка должна проходить автоматически, но при динамической замене компонент, неизвестно когда будет удалена подписка — везде нужно будет дублировать явную отписку.
Заменив компонент, я не знаю в каком состоянии система: включено ли питание, в каком положении Switch, мне просто не откуда это взять. Одно из решений — ввести в систему вспомогательное событие. Вспомогательное событие будет просить компоненты опубликовать свои события — PowerEvent и SwitchEvent. Теперь везде нужно позаботиться о публикации и подписке на это событие — система распадается и превращается в паутину.
Компоненты системы знают только об EventAggregator, но означает ли это слабую связанность? Нет. Несмотря на изолированность компонент друг от друга, в системе присутствует очень сильная неявная связь. Сильная связь выражена в наборе событий, которые нужно обрабатывать. Я не могу заменить Switch на другой компонент, не доработав Led. В результате связь между частями системы превращается в узел: сильная, не явная и запутанная.
Что нужно сделать, что бы в схеме было несколько Switch?
Про использование EventAggregator внутри сервисов, которые реализуют некоторый интерфейс и подменяются в зависимости от конфигурации… Лучше не вспоминать.
Откуда ростут проблемы
Использование EventAggregator нарушает 3 из 5 принципов SOLID. Единственность ответственности — подписка/отписка не забота компонентов схемы. Открытость закрытость — при изменении схемы взаимодействия компонентов, нужно править подписку/отписку. Инверсия зависимости — компонент сам решает, на какие события подписываться/отписываться.
3 из 5, а проблем…
P.S. используйте EventAggregator с осторожностью. Для меня EventAggregator — антипаттерн и бед от него намного больше чем пользы.
Комментарии (4)
27 февраля 2017 в 08:42
+1↑
↓
Я не очень понял, что вы предлагаете использовать вместо этого паттерна.
27 февраля 2017 в 09:27
0↑
↓
Вот и мне тоже интересно. Я согласен с автором, но какая альтернатива? Утки? ©
27 февраля 2017 в 09:40 (комментарий был изменён)
0↑
↓
Хорошая альтернатива — реактивное программирование.
27 февраля 2017 в 09:38
+2↑
↓
Использование EventAggregator нарушает 3 из 5 принципов SOLID.
Как-то многовато насчитали. Может проблема не в паттерне, а в не очень удачном его применении?
По SRP в коде модели светодиода не должно быть никаких подписок, только состояние «горит/не горит» и метод «переключить» (вариант «зажечь» и «погасить»). То же с кнопками — состояние и метод для его изменения. Отдельно собственно схема — модель, хранящая ссылки на модели кнопок и светодиодов, и реализующая основную логику методами «нажать кнопку повер», «нажать переключатель». И отдельно собственно подписчик на события интерфейса, преобразующий их в методы схемы.
Можно реализовать на событийной модели и агрегаторе и собственно бизнес-логику, но обязательно должен быть преобразователь событий интерфейса в события модели.
Вообще, если есть предположения, что приложение будет развиваться для моделирования произвольных схем (постоянного тока без учета переходных процессов для начала, для школы например), то я бы сделал «коммутатор», основная внешняя функция которого транслировать события «подано питание» с одного или нескольких входов на один или несколько выходов в зависимости от состояния «таблицы коммутации», плюс обрабатывать события изменения этой таблицы. Такими коммутаторами в схеме являются и кнопки, и светодиоды, и проводники их соединяющие, и даже отсутствующий источник питания. Проводники — вырожденный случай, транслирующие события с одного входа на другой без состояния, светодиоды (и любые подобные устройства) — то же самое, но имеющее побочный эффект, выключатель — почти тот же проводник, но имеющий состояние, указывающее генерировать ли событие на выходе при поступлении события на входе и метод для его переключения. Переключатель — почти тот же выключатель, но с состоянием, указывающим на какой из двух выходов транслировать событие со входа. Источник питания — коммутатор без входов и с одним выходом, который просто генерирует событие при старте моделирования. «Сборка» же схемы будет состоять из инстанцирования коммутаторов (трансляторов событий), инициализации их схем коммутации, привязки к некоторым из них исполнительных устройств, и, главное, подписки их на события друг друга через агрегатор согласно схеме, в идеале описанной декларативно, что позволит менять её бех кодирования, а читать из файлов, бд, или просто UI. Ответственность коммутаторов — транслировать события из агрегатора в агрегатор и, если с коммутатором связано (sic! композиция, а не наследование!) исполнительное устройство, дергать его соответствующий метод.