Как стать автором
Обновить

Implementing FSM

Время на прочтение 7 мин
Количество просмотров 18K
В статье рассказывается о разработанной автором миниатюрной Java библиотеке, позволяющей коротко и наглядно определять конечные автоматы. Библиотека, назовем ее AkerFSM, доступна в Google Code.
В первой части статьи сформулированы предпосылки и требования к библиотеке. Во второй части приводится абстрактный пример использования библиотеки. В третьей части рассмотрены важные моменты устройства самой библиотеки. Четвертая часть посвящена рассмотрению упрощенного примера из реальной жизни, в котором с помощью конечного автомата задается поведение одного из контроллеров в GWT-приложении.


Предпосылки


Существуют различные способы реализации конечного автомата. До момента создания предлагаемой библиотеки, самым удачным я считал способ, при котором используется оператор switch, осуществляющий выбор по текущему номеру состояния. Пример кода можно посмотреть здесь, а сам подход, фигурирующий под названиями «SWITCH-технология» и «Автоматное программирование», подробно описан в находящихся на упомянутом сайте статьях Шалыто и Туккеля.
При всей своей простоте, реализация оператором switch совсем не вписывается в объектно-ориентированную парадигму и из-за этого не позволяет в полной мере использовать возможности современных языков. Поэтому я задался целью создать реализацию, отвечающую следующим требованиям:
  • автомат задается также наглядно и лаконично, как и при использовании оператора switch
  • реализация является объектно-ориентированной
  • реализация поддерживает все возможности автоматного программирования

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

Абстрактный пример


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



Теперь определим сам автомат и метод, который его создаст:



Чтобы использовать автомат, остается создать его объект и вызывать для обработки событий метод handleEvent(), передавая событие в качестве параметра:



Таким образом, предлагаемая реализация позволяет описывать автоматы также наглядно и лаконично, как и в случае с оператором switch — на определение состояния требуется несколько строк кода, а все определение автомата размещено в едином блоке. Применение стандартного форматирования исходников, как и в случае с оператором switch, немного ухудшит картину, но тут уж надо выбирать. Если ваша среда позволяет отключать автоматическое форматирование для заданного фрагмента текста — то вам повезло.

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


Особенности устройства библиотеки


Основными классами библиотеки являются State и FSM, назначение которых очевидно. Оба класса легко могут быть расширены, что будет продемонстрировано ниже.
Классы объявлены следующим образом:




Дженерик STATES задает enum, в котором хранится множество состояний автомата. Дженерик EVENT задает класс для обрабатываемых автоматом событий. В реальном приложении в EVENT будет указываться базовый класс события, используемый в вашем событийном механизме. В рассматриваемых примерах для упрощения используется String.
Три метода класса State: enter(), handleEvent() и exit(), предназначены для переопределения при создании конкретных состояний. enter вызывается, когда автомат переходит в рассматриваемое состояние, handleEvent — при обработке события, и exit, соответственно, когда автомат покидает рассматриваемое состояние. Эти методы реализуют паттерн Template Method, поэтому при их переопределении вызов super не обязателен.
Вместе с классом State при определении автомата может использоваться его потомок, класс SuperState (State и SuperState совместно реализуют аналог паттерна Composite). Назначение SuperState — реализация общего поведения для группы состояний, например группового перехода.
Метод State.toString() возвращает название и порядковый номер состояния (да здравствует enum!) Аналогичным образом определены и методы FSM.toString() и SuperState.toString().
Поведение класса FSM при вызове метода handleEvent() следующее:
  • вызвать handleEvent() текущего состояния и текущих групп состояний
  • определить следующее состояние (групповые переходы имеют приоритет над обычными)
  • вызвать exit() текущего состояния и всех групповых состояний, которые будут покинуты при переходе
  • вызвать enter() нового состояния и всех групповых состояний, в которые попадаем при переходе
  • выполнять переходы до тех пор, пока результат определения следующего состояния не станет равен null

Кроме этого, FSM реализует 14 событий, для которых можно назначить обработчики, переопределяя либо конкретные методы-обработчики событий, либо метод, вызываемый при возникновении любого из событий. Примером использования обработчиков событий являются классы MonitoredFSM и LoggedFSM.
Наилучший способ более детально разобраться в поведении библиотечных классов — прогнать в отладчике JUnit-тесты, идущие в комплекте с библиотекой. Эти тесты специально были написаны таким образом, чтобы быть примером использования библиотеки и иллюстрацией логики ее работы.

Бонус Исходные коды библиотеки нормально компилируются GWT


Пример из реальной жизни


Теперь рассмотрим пример, более приближенный к реальной жизни. Создадим GWT-приложение, по нажатию кнопки отображающее на экране некую форму. При отображении формы необходимо в два этапа загрузить данные с сервера — сначала выполняется загрузка конфигурации формы, а потом — загрузка данных для отображения на форме. Форма может быть закрыта по нажатию кнопки. Процесс загрузки может быть прерван пользователем. В процессе загрузки должен отображаться прогресс-индикатор с названием выполняемой операции.
Автомат будет иметь пять состояний:



Кроме обычных состояний, определим также два групповых состояния. Первое будет включать состояния LOADCONFIG и LOADDATA и служить для обработки события «RPCFailure». Второе будет включать состояния LOADCONFIG, LOADDATA и SHOW и служить для обработки события «HideEvent».
Осталось реализовать прогресс-бар. Сделать это можно очень элегантным и универсальным способом, который подойдет не только для нашего абстрактного приложения, но и для большинства аналогичных задач.
Во-первых, используем вместо FSM класс MonitoredFSM, который реализует паттерн Observer. Это позволит нам подключить свой обработчик события смены состояния автомата (другой способ — самим переопределить метод onAfterSwitchState() класса FSM).
Во-вторых, породим от класса State класс ProgressState. Он будет отличаться от предка наличием задаваемого в конструкторе описания состояния.
В-третьих, определим callback для нашего экземпляра класса MonitoredFSM следующим образом:



При каждом переходе в состояние типа ProgressState этот callback будет отображать прогресс-индикатор, в котором будет указано описание текущего состояния, номер текущего состояния и общее количество состояний. При переходе в обычное состояние прогресс-индикатор будет исчезать.
Как можно заметить, я слегка схалявил с отображаемыми номерами текущего состояния и общего количества состояний (будет выводиться «2/5» и «3/5», в то время как более логичным с точки зрения пользователя было бы выводить «1/2» и «2/2»). Возможные решения этой проблемки описывать не буду, лишь скажу, что полезным может оказаться класс EnumSet и его статические методы.
Собрав все вместе, получим следующее определение автомата (в состояниях LOADCONFIG и LOADDATA проиллюстрированы разные способы обработки внешних воздействий):



Обработчики событий нажатия кнопок будут иметь следующий вид:



Обработчики RPC-запросов будут иметь следующий вид:



Как следует из двух последних фрагментов кода, мы одним махом разобрались с задачей аккуратной реализации асинхронной части GWT-приложения, создав для этого простой и универсальный паттерн. Причем паттерн этот идентичен и для обработчиков RPC-запросов, и для обычных обработчиков событий.
Исходные тексты библиотеки доступны в Google Code. В папке trunk/fsm находится сама библиотека и JUnit-тесты, а в папке trunk/gwt_fsm — рассмотренное в качестве примера GWT-приложение.

Выводы


Библиотека AkerFSM поддерживает все возможности автоматного программирования:
  • явное выделение и кодирование состояний конечного автомата
  • наглядное и лаконичное определение конечного автомата, внешне похожее на оператор switch
  • поддержка автоматов разных типов (Мили, Мура, смешанный)
  • поддержка событийных и вычислительных автоматов (причем с возможностью смешивать две эти модели в одном автомате)
  • поддержка вложенных автоматов
  • поддержка групповых переходов
  • возможность логгирования в терминах конечного автомата

Кроме поддержки перечисленных возможностей автоматного программирования, библиотека AkerFSM обладает двумя принципиальными достоинствами:
  1. Бизнес-логика отделена от технологического кода. Действительно, логика работы конкретного автомата (бизнес-логика) задается отдельно от кода, реализующего саму модель конечного автомата. Реализацию модели при этом можно переопределять и расширять независимо от конкретных автоматов, «декорируя» бизнес-логику требуемыми технологическими операциями.
  2. Библиотека является объектно-ориентированной, при этом базовые классы библиотеки сделаны легко расширяемыми.

Благодаря этим достоинствам, возникает множество преимуществ и возможностей для расширения. Перечислим некоторые из них
  1. Возможность в одном блоке кода определять действия, выполняемые при проверке условий перехода, при входе в состояние и при выходе из него.
  2. В отличие от упомянутых выше публикаций по автоматному программированию, в которых «групповой переход» присутствует лишь в виде элемента графической нотации, и никак не подержан в имплементации, библиотека AkerFSM полноценно реализует понятие «группового состояния». Для группового состояния, также как и для обычного, можно определить условия переходов, а также действия, выполняемые при входе в групповое состояние и выходе из него.
  3. Переход от модели конечного автомата к модели сети Петри осуществляется несложным расширением класса FSM.
  4. Также просто можно создать модель автомата, умеющую хранить историю переходов.
  5. Реализация паттерна Strategy совместно с классом FSM позволяет отделить автомат от класса, которым он управляет. Это, в частности, дает возможность проводить JUnit тестирование автомата, полностью изолировав его от остальной части приложения.
  6. Класс State (совместно с классом FSM) реализует паттерн State. Простейший вариант его использования в этой роли требует лишь определить интерфейс со специфическими для вашей задачи операциями и реализовать этот интерфейс в необходимом количестве потомков класса State. Хотя «Банда четырех» в описании паттерна State и пишет о том, что определение логики переходов внутри конкретных состояний вносит «реализационные зависимости между подклассами», архитектура библиотеки AkerFSM лишена этого недостатка. В нашем случае логика переходов между состояниями определяется конечным автоматом и записывается в конкретных классах состояний, но зависимость между ними при этом отсутствует.
  7. Возможно расширение класса FSM, которое позволит изменять определение автомата в процессе выполнения (добавлять, удалять и заменять состояния).


Подводя итог, можно утверждать, что библиотека AkerFSM существенно превосходит реализации, описанные в книге Полкарповой, Шалыто «Автоматное программирование».

PS. Уверен, что на Groovy или на Ruby аналогичную реализацию можно сделать еще более красивой.

Теги:
Хабы:
+17
Комментарии 31
Комментарии Комментарии 31

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн