Реализация систем, управляемых событиями

Использование конечных автоматов

Авторы: А.Рахимбердиев
А.Ксенофонтов
Е.Адаменков
Д.Антонов
Р.Степанов

Источник: RSDN Magazine #5-2005
Опубликовано: 02.03.2006
Версия текста: 1.0
Введение
Практическое использование конечных автоматов
Традиционный подход
Конечные автоматы и UML
Реализация на C++
Автоматическое генерирование кода
Пример: сценарий авторизации пользователя
Шаг первый: описание конечного автомата на диаграмме состояний
Шаг второй: реализация классов C++
Выводы и перспективы

Введение

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

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

Практическое использование конечных автоматов

Threads are for people who can't
program state machines
Alan Cox

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

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

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

Далее в этом разделе мы подробно описываем каждый из упомянутых компонентов.

Традиционный подход

В простейшем случае реализация системы, реагирующей на внешние события, выглядит подобным образом:

if ( /* Event A */ ) 
{
  // обработка события A
  ...
} 
else if ( /* Event B */ ) 
{
  // обработка события B
  ...
} 
else ...

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

enum Event 
{
  EventA,
  EventB,
  ...
};

Event next_event();

switch (next_event()) 
{
  case EventA:
    // обработка события A
    ...
  case EventB:
    // обработка события B
    ...

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

switch (next_event()) 
{
  case EventA:
    switch (substate_a) {
  case StateA_1:
    // обработка события
    substate_a = StateA_2;
    break;
  case StateA_2:
    ...

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

Использование объектно-ориентированного дизайна и, в частности, паттерна проектирования State, позволяет значительно улучшить легкость и удобство модификаций поведения системы. В качестве типичного примера использования паттерна State можно привести следующий фрагмент кода:

class State 
{
  virtual State handle_a_1() = 0;
  ...
  virtual State handle_a_m() = 0;
}

class Automaton 
{
  State current_state;

  Automaton(State initial_state) 
  {
    currentState = initial_state;
  }

  void handle_a_1() 
  {
    currentState = currentState.handle_a_1();
  }
  ...
  void handle_a_m() 
  {
    current_state = currentState.handle_a_m();
  }
};

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

Конечные автоматы и UML

Реализация на C++

Автоматическое генерирование кода

Пример: сценарий авторизации пользователя

Шаг первый: описание конечного автомата на диаграмме состояний

Шаг второй: реализация классов C++

Выводы и перспективы


Полная версия этой статьи опубликована в журнале RSDN Magazine #5-2005. Информацию о журнале можно найти здесь