Повторяя на этом частном примере эволюцию, пройденную программированием в целом, перейдем от низкоуровневого подхода goto к иерархической структуре, создаваемой при проектировании сверху вниз. Это решение принадлежит общему стилю, известному как "структурное", хотя этот термин следует использовать с осторожностью.
ОО-решение является, конечно же, структурным, соответствуя духу "структурного программирования", как оно изначально было введено Э. Дейкстрой.
Первым шагом на пути улучшения нашего решения будет придание центральной роли алгоритму обхода в структуре ПО. Диаграмма переходов - это всего лишь одно из свойств нашей системы, и нет никаких оснований, чтобы она правила над всеми частями системы. Ее отделение от остального алгоритма позволит, по крайней мере, избавиться от goto. Можно ожидать и большей общности, поскольку диаграмма переходов специфична для приложения, такого как резервирование авиабилетов, в то время как алгоритм обхода графа более универсален.
Что представляет собой диаграмма переходов? Абстрактно это функция transition, имеющая два аргумента - состояние и выбор пользователя. Функция transition (s, c) возвращает новое состояние, определяемое пользовательским выбором c в состоянии s. Здесь слово "функция" используется в его математическом смысле. На программном уровне можно выбрать реализацию transition либо функцией в программистском смысле, либо структурой данных, например массивом. На данный момент решение можно отложить и рассматривать transition просто как абстрактное понятие.
В добавление к функции transition необходимо спроектировать начальное состояние initial, - точку, в которой начинаются все сессии, и одно или несколько заключительных состояний как булеву функцию is_final. И снова речь идет о функции в математическом смысле, независимо от ее возможной реализации.
Зададим функцию transition в табличной форме со строками, представляющими состояние, и столбцами, отображающими пользовательский выбор:
Состояние | Выбор | |||
---|---|---|---|---|
0 | 1 | 2 | 3 | |
1 (Initial) | -1 | 0 | 5 | 2 |
2 (Flights) | -1 | 0 | 1 | 3 |
3 (Seats) | 0 | 2 | 4 | |
4 (Reserv.) | 0 | 3 | 5 | |
5 (Confirm) | 0 | 4 | 1 | |
0 (Help) | Return | |||
-1 (Final) |
Соглашения, используемые в таблице: здесь в состоянии Help с идентификатором 0 задан специальный переход Return, возвращающий в состояние, запросившее справку, задано также ровно одно финальное состояние -1. Эти соглашения не являются необходимыми для дальнейшего обсуждения, но позволяют проще сделать таблицу. |
Следуя традиционным рекомендациям декомпозиции сверху вниз, выберем "вершину" - главную функцию нашей системы. Это должна быть, очевидно, программа execute_session, описывающая выполнение полной интерактивной сессии.
Непосредственно ниже (уровень 2) найдем операции, связанные с состояниями: определение начального и конечного состояний, структуру переходов и функцию execute_state, описывающую действия, выполняемые в каждом состоянии. На нижнем уровне 1 найдем операции, определяющие execute_state: отображение панели на экране и другие. Заметьте, что и это решение, также как и ОО-решение, описываемое чуть позже, отражает "реальный мир", в данном случае включающий состояния и элементарные операции данного мира. В этом примере и во многих других не в реальности мира состоит важная разница между ОО-подходом и другими решениями, а в том, как мы моделируем этот мир.
При написании программы execute_session попытаемся сделать наше приложение максимально независимым. (Наша нотация выбрана в соответствии с примером. Цикл repeat until заимствован из Pascal.)
execute_session is -- Выполняет полную сессию интерактивной системы local state, next: INTEGER do state := initial repeat execute_state (state, >next) -- Процедура execute_state обновляет значение next state := transition (state, next) until is_final (state) end end
Это типичный алгоритм обхода диаграммы переходов. (Те, кто писал лексический анализатор, узнают образец.) На каждом этапе мы находимся в состоянии state, вначале устанавливаемом в initial ; процесс завершается, когда состояние удовлетворяет is_final. Для состояний, не являющихся заключительными, вызывается execute_state, принимающее текущее состояние и возвращающее в аргументе next выбор перехода, сделанный пользователем. Функция transition определяет следующее состояние.
Техника, используемая в процедуре execute_state, изменяющая значение одного из своих аргументов, никогда не подходит для хорошего ОО-проекта, но здесь она вполне приемлема. Для того чтобы сделать ее явной, используется "флажок" для " out " аргумента - next со стрелкой.
Для завершения проекта следует определить процедуру execute_state, описывающую действия, выполняемые в каждом состоянии. Ее тело реализует содержимое блока начальной goto -версии.
execute_state (in s: INTEGER; out c: INTEGER) is -- Выполнить действия, связанные с состоянием s, -- возвращая в c выбор состояния, сделанный пользователем local a: ANSWER; ok: BOOLEAN do repeat display (s) read (s, a) ok := correct (s, a) if not ok then message (s, a) end until ok end process (s, a) c := next_choice (a) end
Здесь вызываются программы уровня 1 со следующими ролями:
Тип ANSWER объекта, представляющего ответ пользователя, не будет уточняться. Значение a этого типа глобально представляет ввод пользователя, включающий и выбор следующего шага ( ANSWER фактически во многом подобен классу, даже если остальная структура не является объектной.)
Для получения работающего приложения необходимо задать реализации программ уровня 1: display, read, correct, message и process.