Как облегчить обслуживание кода, управляемого событиями?


16

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

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

Это может привести к тонким и сложным проблемам отладки, когда кто-то добавляет некоторые новые обработчики событий.

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

Затем диаграмма последовательности начинает выглядеть сложнее, время, затрачиваемое на выяснение того, что происходит, увеличивается, и сеанс отладки становится грязным (точка останова на диспетчере обработчиков во время итерации на обработчиках, особенно радует асинхронный обработчик и некоторая фильтрация поверх него).

//////////////
Пример

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

Теперь пользователи компонента (и мы тоже пользователь компонента) могут добавлять некоторые обработчики для внесения некоторых изменений в поведение (изменить запрос, журналы, анализ данных, фильтрацию данных, массирование данных, необычную анимацию пользовательского интерфейса, объединить несколько последовательных запросов в цепочку). , без разницы). Поэтому некоторые обработчики должны выполняться до / после некоторых других, и они регистрируются из множества разных точек входа в приложение.

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

Этот дизайн появился, потому что использование наследования начинало быть полным беспорядком. Система событий используется в некой композиции, в которой вы еще не знаете, какими будут ваши композиции.

Конец примера
//////////////

Поэтому мне интересно, как другие люди занимаются этим видом кода. И при написании, и при чтении.

Есть ли у вас какие-либо методы или инструменты, позволяющие вам писать и поддерживать такой код без особых проблем?


Вы имеете в виду, помимо рефакторинга из логики обработчиков событий?
Теластин

Документируйте, что происходит.

@Telastyn, я не уверен, что полностью понимаю, что вы подразумеваете под «помимо рефакторинга логики из обработчиков событий».
Гийом

@Thorbjoern: посмотрите мое обновление.
Гийом

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

Ответы:


7

Я обнаружил, что обработка событий с использованием стека внутренних событий (точнее, очереди LIFO с произвольным удалением) значительно упрощает программирование на основе событий. Это позволяет разделить обработку «внешнего события» на несколько меньших «внутренних событий» с четко определенным состоянием между ними. Для получения дополнительной информации см. Мой ответ на этот вопрос .

Здесь я представляю простой пример, который решается по этой схеме.

Предположим, что вы используете объект A для выполнения некоторого обслуживания, и вы даете ему обратный вызов, чтобы сообщить вам, когда это будет сделано. Однако A таков, что после вызова вашего обратного вызова может потребоваться дополнительная работа. Опасность возникает, когда в рамках этого обратного вызова вы решаете, что вам больше не нужен A, и вы так или иначе уничтожаете его. Но вам звонят из A - если A, после вашего обратного вызова, не может определить, что он был уничтожен, может произойти сбой при попытке выполнить оставшуюся работу.

ПРИМЕЧАНИЕ. Это правда, что вы можете выполнить «уничтожение» другим способом, например, уменьшить счетчик операций, но это просто приведет к промежуточным состояниям, а также к дополнительному коду и ошибкам при их обработке; Лучше, чтобы А просто прекратил работать полностью после того, как вам это больше не нужно, за исключением продолжения в каком-то промежуточном состоянии.

В моем паттерне A просто планировал дальнейшую работу, которую ему необходимо выполнить, помещая внутреннее событие (задание) в очередь LIFO цикла событий, затем переходя к обратному вызову и немедленно возвращаясь к циклу событий . Этот кусок кода больше не представляет опасности, поскольку A просто возвращается. Теперь, если обратный вызов не уничтожает A, то заданное задание в конечном итоге будет выполнено циклом обработки событий, чтобы выполнить свою дополнительную работу (после того, как обратный вызов завершен и все его заданные задания, рекурсивно). С другой стороны, если обратный вызов уничтожает A, деструктор A или функция deinit могут удалить заданное задание из стека событий, неявно предотвращая выполнение заданного задания.


7

Я думаю, что правильное ведение журнала может помочь довольно много. Убедитесь, что каждое выброшенное / обработанное событие где-то зарегистрировано (вы можете использовать для этого каркасы журналирования). Когда вы отлаживаете, вы можете просмотреть журналы, чтобы увидеть точный порядок выполнения вашего кода, когда произошла ошибка. Часто это действительно поможет сузить причину проблемы.


Да, это полезный совет. Количество произведенных журналов может быть пугающим (я помню, как мне было трудно найти причину цикла в обработке событий очень сложной формы.)
Гийом

Возможно, одно из решений - записать интересную информацию в отдельный файл журнала. Кроме того, вы можете назначить UUID каждому событию, чтобы вам было легче отслеживать каждое событие. Вы также можете написать какой-либо инструмент отчетности, который позволяет извлекать конкретную информацию из файлов журнала. (Или, в качестве альтернативы, используйте переключатели в коде для записи различной информации в разные файлы журнала).
Джорджио

3

Поэтому мне интересно, как другие люди занимаются этим видом кода. И при написании, и при чтении.

Модель управляемого событиями программирования в некоторой степени упрощает кодирование. Вероятно, он эволюционировал как замена больших операторов Select (или case), используемых в более старых языках, и приобрел популярность в ранних средах разработки Visual, таких как VB 3 (не цитируйте меня по истории, я не проверял это)!

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

Иногда разработчики стремятся предоставить функциональность GUI, которая требует такой зависимости от события, но на самом деле не существует реальной альтернативы, которая была бы значительно проще.

Суть в том, что техника не плохая, если ее использовать с умом.


Есть ли альтернативный дизайн, чтобы избежать «хуже, чем спагетти»?
Гийом

Я не уверен, что есть простой способ без упрощения ожидаемого поведения графического интерфейса, но если вы перечислите все свои пользовательские взаимодействия с окном / страницей и для каждого точно определите, что будет делать ваша программа, вы можете начать группировать код в одно место и сразу несколько событий для группы подпрограмм / методов, которые отвечают за реальную обработку запроса. Также может помочь отделение кода, который обслуживает только GUI, от кода, выполняющего внутреннюю обработку.
NoChance

Необходимость опубликовать это сообщение возникла в результате рефакторинга, когда я перешел из ада наследования в нечто, основанное на событии. В какой-то момент это действительно лучше, но в какой-то другой это довольно плохо ... Так как у меня уже были некоторые проблемы с «событиями, которые стали дикими» в каком-то графическом интерфейсе, мне интересно, что можно сделать, чтобы улучшить обслуживание таких код нарезанный событием.
Гийом

Событийное программирование намного старше, чем VB; он присутствовал в графическом интерфейсе SunTools, и до этого я, кажется, помню, что он был встроен в язык Simula.
Кевин Клайн

@ Гийом, я думаю, ты закончил разработку сервиса. Мое вышеописанное описание было основано главным образом на событиях GUI. Вам (действительно) нужен этот тип обработки?
NoChance

3

Я хотел обновить этот ответ, так как я получил некоторые моменты эврики с тех пор, как после "выравнивания" и "выравнивания" потоков управления и сформулировал некоторые новые мысли по этому вопросу.

Сложные побочные эффекты против сложных потоков управления

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

Я легко могу рассуждать о коде, который вызывает 4 различных побочных эффекта, если они применяются с очень простым потоком управления, таким как последовательный forцикл. Мой мозг может терпеть последовательный цикл, который изменяет размеры и перемещает элементы, анимирует их, перерисовывает их и обновляет некоторый вспомогательный статус. Это достаточно легко понять.

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

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

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

Упростите поток управления или побочные эффекты

Итак, что вы делаете, когда сталкиваетесь с вышеописанным сценарием, который так трудно понять? Стратегия заключается в том, чтобы либо упростить поток управления, либо побочные эффекты.

Широко применяемая стратегия упрощения побочных эффектов заключается в поддержке отложенной обработки. Используя событие изменения размера графического интерфейса в качестве примера, обычное искушение может состоять в том, чтобы повторно применить макет графического интерфейса, изменить положение и изменить размер дочерних виджетов, вызвать другой каскад приложений макета и изменить размер и переместить вниз по иерархии вместе с перерисовкой элементов управления, возможно, вызывая некоторые уникальные события для виджетов с настраиваемым поведением изменения размера, которые запускают больше событий, приводящих к тому, кто знает, где и т. д. Вместо того, чтобы пытаться делать все это за один проход или спамить очередь событий, одним из возможных решений является спуск по иерархии виджетов. и отметьте, какие виджеты нуждаются в обновлении макетов. Затем на более позднем, отложенном проходе, который имеет прямой последовательный поток управления, повторно примените все макеты для виджетов, которым это необходимо. Затем вы можете отметить, какие виджеты нужно перекрасить. Снова в последовательном отложенном проходе с прямым потоком управления перекрасьте виджеты, помеченные как нуждающиеся в перерисовке.

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

Это связано с определенными накладными расходами на обработку, но затем может открыть двери, скажем, для выполнения этих отложенных проходов параллельно, что потенциально позволяет вам получить еще более эффективное решение, чем вы начали, если производительность вызывает сомнения. Обычно производительность не должна быть большой проблемой в большинстве случаев. Самое главное, что, хотя это может показаться спорным, я обнаружил, что об этом гораздо проще рассуждать. Намного легче предсказать, что происходит и когда, и я не могу переоценить значение, которое может иметь возможность легче понять происходящее.


2

То, что сработало для меня, - это сделать каждое событие самостоятельным, без ссылок на другие события. Если они приходят в asynchroniously, вы не имеете последовательность, таким образом пытаясь выяснить , что происходит , в каком порядке не имеет смысла, кроме того , что невозможно.

В итоге вы получаете набор структур данных, которые читаются, модифицируются, создаются и удаляются дюжиной потоков в произвольном порядке. Вы должны делать очень правильное многопоточное программирование, что не так просто. Вы также должны подумать о многопоточности, как в «С этим событием я собираюсь посмотреть на данные, которые у меня есть в определенный момент, независимо от того, какой была микросекунда раньше, независимо от того, что только что изменило их и безотносительно того, что будут делать с этим 100 потоков, ожидающих, чтобы я снял блокировку. Затем я внесу свои изменения, основываясь на этом, даже на том, что я вижу.

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

Рассматривать каждый поток как полностью независимый объект сложно (из-за многопоточности жесткого ядра), но выполнимо. «Масштабируемый» может быть словом, которое я ищу. В два раза больше событий требует только вдвое больше работы, а может быть, только в 1,5 раза больше. Попытка скоординировать больше асинхронных событий быстро похоронит вас.


Я обновил свой вопрос. Вы абсолютно правы в отношении последовательности и асинхронности. Как бы вы справились со случаем, когда событие X должно выполняться после обработки всех остальных событий? Похоже, мне нужно создать более сложный менеджер обработчиков, но я также хочу избежать слишком большой сложности для пользователей.
Гийом

В вашем наборе данных есть переключатель и некоторые поля, которые устанавливаются при чтении события X. Когда все остальные обработаны, вы проверяете переключатель и знаете, что вам нужно обработать X, и у вас есть данные. На самом деле коммутатор и данные должны стоять самостоятельно. Когда установлено, вы должны думать: «Я должен делать эту работу», а не «Я должен обращаться с Х». Следующая проблема: как вы узнаете, что события сделаны? а что если вы получите 2 или более событий X? В худшем случае вы можете запустить циклический поток обслуживания, который проверяет ситуацию и может действовать по собственной инициативе. (Нет ввода в течение 3 секунд? Переключатель X установлен? Затем запустите код выключения.
RalphChapin

2

Похоже, вы ищете State Machines & Event-Driven деятельности .

Однако вы также можете посмотреть пример рабочего процесса разметки конечного автомата .

Вот вам краткий обзор реализации конечного автомата. Состояние машины документооборота состоит из состояний. Каждое состояние состоит из одного или нескольких обработчиков событий. Каждый обработчик события должен содержать задержку или IEventActivity в качестве первого действия. Каждый обработчик события также может содержать действие SetStateActivity, которое используется для перехода из одного состояния в другое.

Каждый рабочий процесс конечного автомата имеет два свойства: InitialStateName и CompletedStateName. Когда создается экземпляр рабочего процесса конечного автомата, он помещается в свойство InitialStateName. Когда конечный автомат достигает свойства CompletedStateName, он завершает выполнение.


2
Хотя это может теоретически ответить на вопрос, было бы предпочтительным включить здесь основные части ответа и предоставить ссылку для справки.
Томас Оуэнс

Но если у вас есть десятки обработчиков, подключенных к каждому состоянию, вы не будете так хороши, пытаясь понять, что происходит ... И каждый компонент, основанный на событиях, не может быть описан как конечный автомат.
Гийом

1

Событийный код не является реальной проблемой. На самом деле, у меня нет проблем следования логике даже в управляемом коде, где обратный вызов явно задан или используются обратные вызовы в строке. Например, обратные вызовы в стиле генератора в Tornado очень просты для понимания.

Что действительно трудно отлаживать, так это динамически генерируемые вызовы функций. Паттерн (анти?), Который я бы назвал Фабрикой обратного вызова из ада. Однако этот тип функциональных фабрик одинаково трудно отлаживать в традиционном потоке.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.