Pull to refresh

EventAggregator — антипаттерн

Reading time6 min
Views17K
Перед прочтением необходимо почитать о шаблоне EventAggregator. 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<Power>
    {
    }
    public enum SwitchConnection
    {
        Connection1,
        Connection2,
    }

    class SwitchEvent : PubSubEvent<SwitchConnection>
    {
    }


Код управления питанием
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<PowerEvent>().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<SwitchEvent>().Publish(_switch ? SwitchConnection.Connection1 : SwitchConnection.Connection2);
            }
        }

    }


Код светодиода
/// <summary>
    /// ViewModel светодиода.
    /// </summary>
    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<PowerEvent>().Subscribe(OnPowerChanged);
            aggregator.GetEvent<SwitchEvent>().Subscribe(OnSwitch);

            Update();
        }

        /// <summary>
        /// Свет от светодиода.
        /// </summary>
        public Brush Light
        {
            get { return _currentlight; }
            private set
            {
                SetProperty(ref _currentlight, value);
            }
        }

        /// <summary>
        /// Обработчик переключателя.
        /// </summary>
        void OnSwitch(SwitchConnection connection)
        {
            if (SetProperty(ref _currentConnection, connection))
                Update();
        }

        /// <summary>
        /// Обработчик питания.
        /// </summary>
        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;
        }

    }


Xaml разметка
<Window x:Class="AggregatorAntiPattern.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:AggregatorAntiPattern"
        mc:Ignorable="d"
         Height="350" Width="525">
    <Window.Resources>
        <Style TargetType="Path" x:Key="Light">
            <Setter Property="Stroke" Value="Black" />
            <Setter Property="StrokeThickness" Value="2" />
            <Setter Property="Fill" Value="{Binding Light}" />
            <Setter Property="Data">
                <Setter.Value>
                    <EllipseGeometry RadiusX="10" RadiusY="10" />
                </Setter.Value>
            </Setter>
        </Style>
        <Style TargetType="Line" x:Key="Connection">
            <Setter Property="Stroke" Value="Black" />
            <Setter Property="StrokeThickness" Value="1" />
        </Style>
    </Window.Resources>
    <Canvas Margin="20">
        <ToggleButton Canvas.Top="120" Content=" Power " DataContext="{Binding PowerVM}" IsChecked="{Binding Power}" />
        <Line Canvas.Top="130" Canvas.Left="40" X1="0" X2="90" Y1="0" Y2="0" Style="{StaticResource Connection}" />
        <ToggleButton Canvas.Top="120" Canvas.Left="120" Content=" Switch " DataContext="{Binding SwitchVM}" IsChecked="{Binding Switch}" />
        <Line Canvas.Top="130" Canvas.Left="165" X1="0" X2="77" Y1="0" Y2="-30" Style="{StaticResource Connection}" />
        <Line Canvas.Top="130" Canvas.Left="165" X1="0" X2="77" Y1="0" Y2="30" Style="{StaticResource Connection}" />
        <Path Canvas.Top="100" Canvas.Left="250" DataContext="{Binding Connection1Light}" Style="{StaticResource Light}" />
        <Path Canvas.Top="160" Canvas.Left="250" DataContext="{Binding Connection2Light}" Style="{StaticResource Light}" />
    </Canvas>
</Window>


Связующий код
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 – антипаттерн и бед от него намного больше чем пользы.
Tags:
Hubs:
Total votes 19: ↑12 and ↓7+5
Comments52

Articles