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

Опыт применения КА (FSM) в веб-интерфейсе на Python

Время на прочтение6 мин
Количество просмотров6.2K
Введение

Недавно мне потребовалось приделать к одной программке веб-интерфейс.
Точнее какой-никакой веб-интерфейс уже был, но работал он в только в одну сторону, оказывал результаты работы ядра программы. А потребовалось ввести интерактивность.

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

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

У меня есть привычка для запутанных программ рисовать блок-схемки. Для программы, работающей в режиме, когда есть выраженные состояния, в которых может находиться программа, события, на которые она должна реагировать (например нажатия кнопок) и переходы из одного состояния в другое, идеально подходит концепция конечного автомата или КА, по заграничному это будет FSM или Finit State Machine. Однажды, еще под ДОС-ом я разобрался с менюшками одной замудренной программы только тогда, когда нарисовал для нее диаграмму диаграмму состояний КА.

Что такое КА (конечный автомат)

Те кто знают, что это такое, этот раздел могут совершенно спокойно пропустить.

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

Работа КА, собственно и состоит в переходах из одних состояний в другие.

Что может дать применение подхода КА в данном случае?

В данном случае, это для описания работы веб-интерфейса

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

Среда

При разработке данного софта использовалось следующее программное обеспечение:
Язык: Python 2.5.4
Вебсервер: Cherrypy 3.1.1

Реализация и работа кода

Организация кода КА

То, что в качестве вебсервера использован cherrypy, диктовало некоторые требования к организации кода.

Код был организован примерно так: Весь веб интерфейс реализован в классе HomePage.
Узловая точка, как веб интерфейса, так и для КА, метод index()

КА так же реализован в классе HomePage. Код КА можно разделить на три части:

1. Код в методе index(). Получает от браузера параметр 'tostate' и вызывает методы управления, реализует управляющий цикл КА.

2. Методы управления КА. fsm_transition(self,to_state), fsm_process_methods(self,fsm_data)

3. Словарь данных КА (self.fsm_trans).

4. Интерфейсные методы КА. Это методы, вызываемые при переходах из одного состояния КА в другое и выполняющие необходимые при этих переходах конкретные действия.

Словарь данных КА

Описание работы кода лучше всего начать со словаря данных КА. Вот он:

self.fsm_trans = {
    # 1
    'init_logo':{'type':'Show','methods':[self.fsm_logo_show]},
    # 2
    'logo_logo':{'type':'Show','methods':[self.fsm_logo_show]},
.......
    # 6
    'pg1_prj_select_page':{'type':'Show','methods':[self.fsm_project_set_dir,
                                                self.fsm_diag_files_rm,
                                                self.fsm_project_page_algor_start,
                                                self.fsm_set_cur_page_algor_first_page,
                                                self.fsm_set_cur_block_first_block_of_page,
                                                self.fsm_create_cur_page_diag_files,
                                                self.fsm_page_show]},
.....

    # 29
    'f3_pg_block_edit_check_pg_block_edit':{'type':'Act','methods':
                                               [self.fsm_pg_block_edit,
                                                 self.fsm_diag_files_rm,
                                                 self.fsm_create_cur_page_diag_files,
                                                 self.fsm_page_cur_save,
                                                 self.fsm_go_to_page]},
.....


Ключ словаря. Это строка, составленная из названий двух состояний, исходного и следующего. Исходное состояние помнит наш КА в переменной self.fsm_prev_state, следующее поступает из параметра запроса браузера. Например для перехода, обозначенного '# 1', исходное состояние 'init', следующее 'logo', ключ получаем как 'init' + '_' + 'logo'

По ключу расположен опять словарь. В нем два ключа, 'type' и 'methods'.

По ключу 'type' указан тип следующего состояния. Типов состояния два, 'Show' и 'Act'. Тип 'Show' обозначает, что это состояние показа сгенерированной HTML страницы и ожидание следующего запроса от браузера. Тип 'Act' обозначает, что на этом состоянии останавливаться не нужно. Нужно переходить к следующему состоянию.

По ключу 'methods' расположен массив интерфейсных имен методов КА. Это, собственно говоря, последовательность полезных действий, которые необходимо выполнить при переходе из одного состояния в другое. Причем для типа 'Show' последний метод массива должен вернуть сформированную HTML страницу, а для типа 'Act' — название состояния, к которому нужно перейти.

Код КА в методе index()

1  @cherrypy.expose
2  def index(self, block='', tostate='', page='',project='', **data):
    .......
3    try:
4        # FSM go to next state
5        fsm_data = self.fsm_transition(self.http_param['tostate'])
6    # If 'tostate' parameter is not valid for this state
7    except KeyError, error_detail:
8        return self.fsm_error_show('KeyError: ' + str(error_detail))
9    processing_result = self.fsm_process_methods(fsm_data)
10    while True:
11        # If type is 'Show' return html page 
12        if fsm_data['type'] == 'Show':
13            return processing_result
14        # If type is 'Act', FSM to next state
15        # the name of the state is in processing_result
16        if fsm_data['type'] == 'Act':
17            fsm_data = self.fsm_transition(processing_result)
18            processing_result = self.fsm_process_methods(fsm_data)
   


Метод получает параметр 'tostate' из запроса. после этого метод self.fsm_transition() в строке 5 возвращает словарь из словаря данных, в котором находятся тип состояния (Show|ACt) и массив методов для исполнения.

В случае ошибки ключа, т.е. невозможно перейти из данного состояния в нужное, по исключению КА переходит в состояние 'error'. Ошибка ключа может случиться, например, если браузер вернули на несколько шагов по истории и нажали какую-либо ссылку. В этом случае,
отображаемая браузером HTML страница не соответствует состоянию КА и запрос может потребовать перейти в состояние, к которому из текущего состояния не должно быть перехода.

В строке 9 метод self.fsm_process_methods(fsm_data) обрабатывает массив методов перехода и возвращает значение, которое вернул последний метод из массива методов. В случае типа 'Show' это код HTML страницы, метод 'index' возвращает его, а сервер передает его браузеру.
Если тип 'Act', это имя следующего состояния, в строке 17 переводим КА в следующее состояние, в строке 18 обрабатываем методы перехода и так далее, пока не дойдем до типа Show'.

Методы управления КА

# FSM methods
def fsm_transition(self,to_state):
    """
    Change state of FSM to state
    Provide decision what to do
    Arguments:
    to_state - State to go to
    Return:
    """
    # Remember old state
    self.fsm_prev_state = self.fsm_state
    self.fsm_state = to_state
    # Get return value from dictionary
    key = str(self.fsm_prev_state) + '_' + str(self.fsm_state)
    self.fsm_return = self.fsm_trans[key]
    return self.fsm_return

def fsm_process_methods(self,fsm_data):
    """
    Get fsm_data dictionary, process functions and
    return htmal or next state
    Arguments:
    fsm_data - Dictionary, taken from self.fsm_transition() return value
    It is taken from self.fsm_trans dictionary
    Return:
    html text in case 'type':'Show'
    name of the next state in case 'type':'Act'
    """
    last_func = len(fsm_data['methods'])
    i = 0
    for method in fsm_data['methods']:
        i += 1
        if i == last_func:
            return method()         # For last function return its return value
        method()


Тут, думаю, все тривиально.

О выборе метода обработки интерфейсных методов

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

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

Заключение

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

Приложение: Пример диаграммы КА (фрагмент реальной диаграммы)

image
Теги:
Хабы:
+23
Комментарии33

Публикации

Изменить настройки темы

Истории

Работа

Data Scientist
60 вакансий
Python разработчик
132 вакансии

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

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн