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

Перед прочтением необходимо почитать о шаблоне EventAggregator. EventAggregator обеспечивает взаимодействие компонент и сервисов составного приложения, через слабую связанность.

EventAggregator можно найти во многих WPF-каркасах: Mvvm Light -класс Messenger, Catel — класс MessageMediator. Я познакомился с EventAggregator вместе с WPF каркасом Prism. Использование EventAggregator оказалось простым и гибким. Компоненты системы становятся независимыми друг от друга — изменяя один компонент, я не боюсь сломать другой.

При рассмотрении отдельных компонент все так и есть, но поднявшись на уровень работы компонентов в системе, можно разглядеть серьёзные проблемы:

fcf643900e3440f7b5c26f2801976176.jpg

Делюсь моим взглядом на слишком слабую связанность и не явное взаимодействие между частями системы.

Управление светодиодами через EventAggregator


Для управления светодиодами понадобятся: кнопка питания — Power, переключатель с двумя состояниями — Switch и два светодиода — RedLed и BlueLed. На WPF это выглядит, как то так:
adf1e8cd9bb54869a2ff85000177bd12.png

Кнопка 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;
        }

    }


Xaml разметка

    
        
        
    
    
        
        
        
        
        
        
        
    



Связующий код
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?


Прежде чем получить ответ, хорошо подумйте.
6a2595d652d2423098587e5466661099.png

Про использование 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! композиция, а не наследование!) исполнительное устройство, дергать его соответствующий метод.

© Habrahabr.ru