учебники, программирование, основы, введение в,

 

Образец проектирования: многопанельные интерактивные системы

Многопанельные системы
Наша задача - спроектировать систему, представляющую некоторый общий тип интерактивных систем. В этих системах работа пользователя состоит в выполнении ряда этапов, и каждый этап поддерживается полноэкранной диалоговой панелью.
В целом процесс является простым и хорошо определенным. Сессия работы пользователя проходит через некоторое число состояний. В каждом состоянии отображается некоторая панель, содержащая вопросы к пользователю. Пользователь дает требуемые ответы, проверяемые на согласованность (вопросы повторяются, пока не будет дан корректный ответ); затем ответ обрабатывается некоторым образом, например обновляется база данных. Частью пользовательского ответа является выбор следующего шага, интерпретируемый как переход к следующему состоянию, в котором процесс повторяется.
Примером может служить система резервирования авиабилетов, где состояния представляют такие шаги обработки, как User Identification (Идентификация Пользователя), Enquiry on Flights (Запрос Рейса в нужное место и требуемую дату), Enquiry on Seats (Запрос Места на выбранный рейс), Reservation (Резервирование).
Типичная панель для состояния Enquiry on Flights (Запрос Рейса) может выглядеть как на(рисунок иллюстрирует только идею и не претендует на реализм или хороший эргономичный дизайн). Экран показан на шаге, завершающем состояние, ответы пользователя в соответствующих окнах показаны курсивом, реакция системы на эти ответы (показ доступных рейсов) дана жирным шрифтом.
Сессия начинается в начальном состоянии Initial и заканчивается в заключительном состоянии Final. Мы можем представить всю структуру графом переходов, показывающим возможные состояния и переходы между ними. Ребра графа помечены целыми, соответствующими возможному выбору пользователя следующего шага при завершении состояния. На показан граф переходов системы резервирования авиабилетов.
Проблема, возникающая при проектировании и реализации таких приложений, состоит в достижении максимально возможной общности и гибкости. В частности:

  • G1 Граф может быть большим. Довольно часто можно видеть приложения, включающие сотни состояний с большим числом переходов.
  • G2 Структура системы, как правило, изменяется. Проектировщики не могут предвидеть все возможные состояния и переходы. Когда пользователи начинают работать с системой, они приходят с запросами на изменение системы и расширение ее возможностей.
  • G3 В данной схеме нет ничего специфического для конкретного приложения. Система резервирования авиабилетов является лишь примером. Если вашей компании необходимо несколько таких систем для собственных целей или в интересах различных клиентов, то большим преимуществом было бы определить общий проект или, еще лучше, множество модулей, допускающих повторное использование в разных приложениях.

 Первая напрашивающаяся попытка
Давайте начнем с прямолинейной, без всяких ухищрений программной схемы. В этой версии наша система будет состоять из нескольких блоков по одному на каждое состояние системы: BEnquiry, BReservation, BCancellation и т. д. Типичный блок (выраженный не в ОО-нотации этой книги, а в специально подобранной для этого случая, хотя и удовлетворяющей некоторым синтаксическим соглашениям), выглядит следующим образом:
BEnquiry:
"Отобразить панель Enquiry on flights"
repeat
"Чтение ответов пользователя и выбор C следующего шага"
if "Ошибка в ответе" then
"Вывести соответствующее сообщение" end
until not ошибки в ответе end
"Обработка ответа"
case C in
C0: goto Exit,
C1: goto BHelp,
C2: goto BReservation,
...
end
Аналогичный вид имеют блоки для каждого состояния.
Что можно сказать об этой структуре? Ее нетрудно спроектировать, и она делает свое дело. Но с позиций программной инженерии она оставляет желать много лучшего.
Наиболее очевидная критика связана с присутствием инструкций goto (реализующих условные переходы подобно переключателю switch языка C или "Вычисляемый Goto" Fortran), из-за чего управляющая структура выглядит подобно "блюду спагетти" и чревата ошибками.
Но goto - это симптом заболевания, а не настоящая причина. Мы взяли поверхностную структуру нашей проблемы - текущую форму диаграммы переходов - и перенесли ее в алгоритм. Ветвящаяся структура программы является точным отражением графа переходов. Из-за этого наш проект становится уязвимым к любым простым и общим изменениям, о чем уже говорилось выше. Когда кто-то попросит нас добавить состояние и изменить граф переходов, нам придется менять центральную управляющую структуру системы. Нам придется забыть, конечно же, о надеждах повторного использования приложений - цели G3 из нашего списка, требующей, чтобы структура покрывала все возможные приложения подобного вида.


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

Чтобы получить не просто систему, а хорошую систему, придется еще поработать.

Функциональное решение: проектирование сверху вниз

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

Функция переходов

Первым шагом на пути улучшения нашего решения будет придание центральной роли алгоритму обхода в структуре ПО. Диаграмма переходов - это всего лишь одно из свойств нашей системы, и нет никаких оснований, чтобы она правила над всеми частями системы. Ее отделение от остального алгоритма позволит, по крайней мере, избавиться от goto. Можно ожидать и большей общности, поскольку диаграмма переходов специфична для приложения, такого как резервирование авиабилетов, в то время как алгоритм обхода графа более универсален.
Что представляет собой диаграмма переходов? Абстрактно это функция transition, имеющая два аргумента - состояние и выбор пользователя. Функция transition (s, c) возвращает новое состояние, определяемое пользовательским выбором c в состоянии s. Здесь слово "функция" используется в его математическом смысле. На программном уровне можно выбрать реализацию transition либо функцией в программистском смысле, либо структурой данных, например массивом. На данный момент решение можно отложить и рассматривать transition просто как абстрактное понятие.
В добавление к функции transition необходимо спроектировать начальное состояние initial, - точку, в которой начинаются все сессии, и одно или несколько заключительных состояний как булеву функцию is_final. И снова речь идет о функции в математическом смысле, независимо от ее возможной реализации.
Зададим функцию transition в табличной форме со строками, представляющими состояние, и столбцами, отображающими пользовательский выбор:

                   

Таблица 2.1. Таблица переходов

Состояние

Выбор

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 со следующими ролями:

  • display (s) выводит на экран панель, связанную с состоянием s;
  • read (s, a) читает в a ответы пользователя, введенные в окнах панели состояния s;
  • correct (s, a) возвращает true, если и только если a является приемлемым ответом; если да, то process (s, a) обрабатывает ответ a, например, обновляя базу данных или отображая некоторую информацию, если нет, то message (s, a) выводит соответствующее сообщение об ошибке.

Тип ANSWER объекта, представляющего ответ пользователя, не будет уточняться. Значение a этого типа глобально представляет ввод пользователя, включающий и выбор следующего шага (ANSWER фактически во многом подобен классу, даже если остальная структура не является объектной.)
Для получения работающего приложения необходимо задать реализации программ уровня 1: display, read, correct, message и process.

Критика решения

Получили ли мы удовлетворительное решение? Не совсем. Оно лучше, чем первая версия, но все еще далеко от заявленных целей повторного использования и расширяемости.

Статичность

Хотя с первого взгляда кажется, что нам удалось отделить общность, присущую приложениям такого типа, от специфических черт конкретного приложения, в реальности различные модули все еще тесно связаны друг с другом и с выбранным приложением. Главной проблемой остается структура данных, передаваемых в системе. Рассмотрим сигнатуры (типы аргументов и результатов) наших программ:
Замечание, звучащее подобно жалобе, состоит в том, что роль состояний всеобъемлюща. Текущее состояние s появляется как аргумент во всех программах, спускаясь с вершины execute_session, где оно известно как state. Так что кажущаяся простота и управляемость иерархической структуры, показанной на, является ложью или, более точно, фасадом. За спиной формальной элегантности функциональной декомпозиции стоит неразбериха передачи данных. Истинная картина должна включать данные.
В основе технологии лежит битва между функциями и данными (объектами) за управление архитектурой системы. В необъектных подходах функции берут вверх над данными, но затем данные начинают мстить.
Месть проявляется в форме саботажа. Атакуя основания архитектуры, данные не пропускают изменения, - пока, подобно правительству не способному руководить своей перестройкой (в оригинале - perestroikа), система не рухнет под собственной тяжестью.
В этом примере структура рушится из-за необходимости различать состояния. Все программы уровня 1 должны выполнять различные действия, зависящие от состояния s: отображать панель для некоторого состояния, читать и интерпретировать ответы пользователя, определять корректность ответов, - для всех этих задач необходимо знать состояние. Программы должны будут выполнять разбор случаев в форме:

    inspect
        s...
    when Initial then
        ...
    when Enquiry_on_flights then
        ...
    ....
    end

Это приводит к длинной и сложной структуре и, что хуже всего, к неустойчивой системе, - любое добавление состояния потребует изменения всей структуры. Имеет место типичный случай необузданного распределения знаний. Слишком много модулей системы используют одну и ту же информацию - список всех возможных состояний, являющийся предметом изменений.
Если надеяться на получение общего повторно используемого решения, то ситуация еще хуже, чем может показаться. Дело в том, что во всех программах неявно присутствует еще один аргумент - приложение - система резервирования авиабилетов или другая проектируемая система. Так что программы, такие как display, если они действительно носят общий характер, должны знать все состояния всех возможных приложений! Аналогично функция transition должна содержать графы переходов для всех приложений - совершенно нереалистическое предположение.

Объектно-ориентированная архитектура

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

Закон инверсии

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


Закон инверсии
Если программы пересылают друг другу много данных, поместите программы в данные.

Ранее модули строились вокруг операций (таких как execute_session и execute_state) и данные распределялись между программами со всеми неприятными последствиями, с которыми нам пришлось столкнуться. ОО-проектирование ставит все с головы на ноги, - оно использует наиболее важные типы данных как основу модульности, присоединяя каждую программу к тому типу данных, с которым она наиболее тесно связана. Когда объекты одерживают победу, их бывшие хозяева - функции - становятся вассалами данных.
Закон инверсии является ключом получения ОО-проекта из классической функциональной (процедурной) декомпозиции, как это делается в данной лекции. Подобная необходимость возникает в случаях реверс-инженерии, когда существующая необъектная система преобразуется в объектную для обеспечения ее дальнейшей эволюции и сопровождения. К этому приему прибегают в командах, возможно, не столь хорошо знакомых с ОО-проектированием и предпочитающих начинать с привычной функциональной декомпозиции.
Конечно, желательно начинать ОО-проектирование с самого начала, - тогда не придется прибегать к инверсии. Но закон инверсии полезен не только в случае реверс-инженерии или для новичков-разработчиков. И в объектной разработке проекта на фоне объектного ландшафта могут встречать области функциональной декомпозиции. Анализ передачи данных является хорошим способом обнаружения и корректировки подобных дефектов. Если вы обнаружили в структуре, разрабатываемой как объектной, образцы передачи данных, подобные состояниям в данном примере, то это должно привлечь ваше внимание и привести в большинстве случаев к осознанию абстракции данных, не нашедшей еще должного места в проекте.

Состояние как класс

Пример "состояния" является типичным. Такой тип данных, играющий всеобъемлющую роль в передаче данных между программами, является первым кандидатом на роль модуля в ОО-архитектуре, основанной на классах (абстрактно описанных типах данных).
Понятие состояния было важным в оригинальной постановке проблемы, но затем в функциональной архитектуре эта важность была утеряна, - состояние было представлено обычной переменной, передаваемой из программы в программу, как если бы это было существо низкого ранга. Мы уже видели, как оно отомстило за себя. Теперь мы готовы предоставить ему заслуженный статус. STATE должно быть классом, одним из властителей структуры в нашей новой ОО-системе.
В этом классе мы найдем все операции, характеризующие состояние: отображение соответствующего экрана (display), анализ ответа пользователя (read), проверку ответа (correct), выработку сообщения об ошибке для некорректных ответов (message), обработку корректных ответов (process). Мы должны также включить сюда execute_state, выражающее последовательность действий, выполняемых всякий раз, когда сессия достигает заданного состояния (поскольку данное имя было бы сверхквалифицированным в классе, называемом STATE, заменим его именем execute).
Возвращаясь к рисунку, отражающему функциональную декомпозицию, выделим в нем множество программ, принадлежащих классу STATE.

Класс имеет следующую форму:
...class STATE feature
    input: ANSWER
    choice: INTEGER
    execute is do ... end
    display is ...
    read is ...
    correct: BOOLEAN is ...
    message is ...
    process is ...
end

Компоненты input и choice являются атрибутами, остальные - подпрограммами (процедурами и функциями). В сравнении со своими двойниками при функциональной декомпозиции подпрограммы потеряли явный аргумент, задающий состояние, хотя он появится другим путем в клиентских вызовах, таких как s.execute.
В предыдущих подходах функция execute (ранее execute_state) возвращала пользовательский выбор следующего шага. Но такой стиль нарушает правила хорошего проектирования. Предпочтительнее сделать execute командой. Запрос "какой выбор сделал пользователь в последнем состоянии?" доступен благодаря атрибуту choice. Аналогично, аргумент ANSWER подпрограмм уровня 1 заменен теперь закрытым атрибутом input. Вот причина скрытия информации: клиентскому коду нет необходимости обращаться к ответам помимо интерфейса, обеспечиваемого компонентами класса.

Наследование и отложенные классы

Класс STATE описывает не частное состояние, а общее понятие состояния. Процедура execute - одна и та же для всех состояний, но другие подпрограммы зависят от состояния.
Наследование и отложенные классы идеально позволяют справиться с этими ситуациями. На уровне описания класса STATE мы знаем атрибуты и процедуру execute во всех деталях. Мы знаем также о существовании программ уровня 1 (display и др.) и их спецификации, но не их реализации. Эти программы должны быть отложенными, класс STATE, описывающий множество вариантов, а не полностью уточненную абстракцию, сам является отложенным классом. В результате имеем:

indexing
        description: "Состояния приложений, управляемых панелями"
deferred class
    STATE
feature -- Access
    choice: INTEGER
        -- Пользовательский выбор следующего шага 
    input: ANSWER
        -- Пользовательские ответы на вопросы в данном состоянии
feature -- Status report
    correct: BOOLEAN is    
        -- Является ли input корректным ответом?
        deferred
        end
feature -- Basic operations
    display is
        -- Отображает панель, связанную с текущим состоянием
        deferred
        end
    execute is
            -- Выполняет действия, связанные с текущим состоянием,
            -- и устанавливает choice - пользовательский выбор
        local
            ok: BOOLEAN
        do
            from ok := False until ok loop
                display; read; ok := correct
                if not ok then message end
            end
            process
        ensure
            ok
        end
    message is
            -- Вывод сообщения об ошибке, соответствующей input
        require
            not correct
        deferred
        end
    read is
            -- Получить ответы пользователя input и choice
        deferred
        end
    process is
            -- Обработка input
        require
            correct
        deferred
        end
end

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

class ENQUIRY_ON_FLIGHTS inherit
    STATE
feature
    display is
        do
            ...Специфическая процедура вывода на экран...
        end
    ...И аналогично для read, correct, message и process ...
end

Эта архитектура отделяет зерно от шелухи: элементы, общие для всех состояний, отделяются от элементов, специфичных для конкретного состояния. Общие элементы, такие как процедура execute, сосредоточены в классе STATE и нет необходимости в их повторном объявлении в потомках, таких ENQUIRY_ON_FLIGHTS. Принцип Открыт-Закрыт выполняется: класс STATE закрыт, поскольку он является хорошо определенным, компилируемым модулем, но он также открыт, так как можно добавлять в любое время любых его потомков.
Класс STATE является типичным представителем поведенческих классов (behavior classes, см.курса "Основы объектно-ориентированного программирования") - отложенных классов, задающих общее поведение большого числа возможных объектов, реализующих то, что полностью известно на общем уровне (execute) в терминах, зависящих от каждого варианта. Наследование и отложенный механизм задают основу представления такого поведения повторно используемых компонентов.

Описание полной системы

Для завершения проекта следует заняться управлением сессией. При функциональной декомпозиции эту задачу выполняла процедура execute_session - главная программа. Но мы знаем, как сделать это лучшим образом. Как ранее говорилось (см.курса "Основы объектно-ориентированного программирования") главная функция системы, позиционируемая как верхняя функция в проектировании сверху вниз, - это нечто мифическое. Большие программные системы выполняют множество одинаково важных функций. И здесь основанный на АТД подход является предпочтительным. Вся система в целом рассматривается как множество абстрактных объектов, способных выполнять ряд служб (services).
Ранее мы рассмотрели одну ключевую абстракцию - STATE. Какая же абстракция в нашем рассмотрении осталась пропущенной? Ответ очевиден: центральным в нашей системе является понятие APPLICATION, описывающее специфическую интерактивную систему, подобную системе резервирования билетов. Это приводит нас к следующему классу.
Заметьте, все не вошедшие в класс STATE программы функциональной декомпозиции стали теперь компонентами класса APPLICATION:

  • Execute_session - описывает, как выполнять сессию, ее имя теперь разумно упростить и называть просто execute, так как дальнейшую квалификацию обеспечивает имя класса.
  • Initial и is_final - указывают, какие состояния имеют специальный статус в приложении. Конечно же, их разумно включить именно в класс APPLICATION, а не в класс STATE, поскольку они описывают свойства приложения, а не состояния, которое является заключительным или начальным только по отношению к приложению, а не само по себе. При повторном использовании состояние, бывшее заключительным в одном приложении вполне может не быть таковым для другого приложения.
  • Transition - описывает переходы между состояниями приложения.

Все компоненты функциональной декомпозиции нашли свое место в ОО-декомпозиции: одни в классе STATE, другие в APPLICATION. Это не должно нас удивлять. Объектная технология, о чем многократно говорится в этой книге, является прежде всего архитектурным механизмом, в первую очередь предназначенным для организации программных элементов в согласованные структуры. Сами элементы, возможно, нижнего уровня, те же самые или похожие на элементы необъектных решений. Объектные механизмы: абстракция данных, скрытие информации, утверждения, наследование, полиморфизм, динамическое связывание позволяют сделать задачу проектирования более простой, общей и мощной.
Системе управления панелями, изучаемой в данной лекции, всегда необходимы: процедура обхода графа приложения (execute_session, теперь просто execute), чтение ввода пользователя (read), обнаружение заключительного состояния (is_final). Погружаясь в структуру, можно найти одни и те же элементы, независимо от выбранного подхода к проектированию. Что же меняется? - способ группирования элементов, создающий модульную архитектуру.
Конечно, нет необходимости ограничивать себя элементами, пришедшими из предыдущих решений. То, что для функционального решения было завершением процесса - построение функции execute и всего необходимого для ее работы - теперь становится только началом. Существует много других вещей, которые хотелось бы выполнять для подобных приложений:

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

Все эти операции и другие будут в равной степени являться компонентами класса APPLICATION. Здесь нет более или менее важных программ, чем наша бывшая "главная программа", - процедура execute, ставшая теперь обычным компонентом класса, равная среди других, но не первая. Устраняя понятие вершины, мы подготавливаем систему к эволюции и повторному использованию.

Класс приложения

Для завершения рассмотрения класса APPLICATION рассмотрим несколько возможных реализационных решений:

  • Будем нумеровать состояния приложения числами от 1 до n. Заметьте, эти числа не являются абсолютными свойствами состояний, они связаны с определенным приложением, поэтому в классе STATE нет атрибута "номер состояния". Вместо этого, одномерный массив associated_state, атрибут класса APPLICATION, задает состояние, связанное с заданным номером.
  • Представим функцию переходов transition еще одним атрибутом - двумерным массивом размерности n * m, где m - число возможных пользовательских выборов при выходе из состояния.
  • Номер начального состояния хранится в атрибуте initial и устанавливается в подпрограмме choose_initial. Для конечных состояний мы используем соглашение, что переход в псевдосостояние 0 означает завершение сессии.
  • Процедура создания в классе APPLICATION использует процедуры создания библиотечных классов ARRAY и ARRAY2. Последний описывает двумерные массивы и построен по образцу ARRAY; его процедура создания make принимает четыре аргумента, например create a.make (1, 25, 1, 10), а его подпрограммы item и put используют два индекса - a.put (x, 1, 2). Границы двумерного массива a можно узнать, вызвав a.lower1 и так далее.

Вот определение класса, использующего эти решения:

indexing
    description: "Интерактивные приложения, управляемые панелями"
class APPLICATION creation
    make
feature -- Initialization
    make (n, m: INTEGER) is
    -- Создает приложение с n состояниями и m возможными выборами
    do
            create transition.make (1, n, 1, m)
            create associated_state.make (1, n)
    end
feature -- Access
    initial: INTEGER
            -- Номер начального состояния 
feature -- Basic operations
    execute is
            -- Выполняет сессию пользователя 
        local
            st: STATE; st_number: INTEGER
        do    
            from
                st_number := initial
            invariant
                0<= st_number; st_number <= n
            until st_number = 0 loop
                st := associated_state.item (st_number)
                st.execute
            -- Вызов процедуры execute класса STATE.
            -- (Комментарии к этой ключевой инструкции даны в тексте.)
                st_number := transition.item (st_number, st.choice)
            end
        end
feature -- Element change
    put_state (st: STATE; sn: INTEGER) is
            -- Ввод состояния st с индексом sn
        require
            1 <= sn; sn <= associated_state.upper
        do
            associated_state.put (st, sn)
        end
    choose_initial (sn: INTEGER) is
            -- Определить состояние с номером sn в качестве начального
        require
            1 <= sn; sn <= associated_state.upper
        do
            initial := sn
        end
    put_transition (source, target, label: INTEGER) is
            -- Ввести переход, помеченный label,
            -- из состояния с номером source в состояние target
        require
            1 <= source; source <= associated_state.upper
            0 <= target; target <= associated_state.upper
            1 <= label; label <= transition.upper2
        do
            transition.put (source, label, target)
        end
feature {NONE} -- Implementation
    transition: ARRAY2 [STATE]
    associated_state: ARRAY [STATE]
    ... Другие компоненты ...
invariant
    transition.upper1 = associated_state.upper
end -- class APPLICATION

Обратите внимание на простоту и элегантность вызова st.execute. Компонент execute класса STATE является эффективным (полностью реализованным) поскольку описывает известное общее поведение состояний, но его реализация основана на вызове компонентов: read, message, correct, display, process, отложенных на уровне STATE, эффективизация которых выполняется потомками класса, такими как RESERVATION. Когда мы помещаем вызов st.execute в процедуру execute класса APPLICATION, у нас нет информации о том, какой вид состояния обозначает st, но благодаря статической типизации мы точно знаем, что это состояние. Далее включается механизм динамического связывания и в период исполнения st становится связанной с объектом конкретного вида, например RESERVATION, - тогда вызовы read, message и других царственных особ автоматически будут переключаться на нужную версию.
Значение st, полученное из associated_state, представляет полиморфную структуру данных (polymorphic data structure), содержащую объекты разных типов, все из которых согласованы (являются потомками) со STATE. Текущий индекс st_number определяет операции состояния.
Вот как строится интерактивное приложение. Приложение должно быть представлено сущностью, скажем air_reservation, класса APPLICATION. Необходимо создать соответствующий объект:

create air_reservation.make (number_of_states, number_of_possible_choices)

Далее независимо следует определить и создать состояния приложения, как сущности классов-потомков STATE, либо новые, либо уже готовые и взятые из библиотеки повторного использования. Каждое состояние s связывается с номером i в приложении:

air_reservation.put_state (s, i).

Затем одно из состояний выбирается в качестве начального:

air_reservation.choose_initial (i0)

Для установления перехода от состояния sn к состоянию с номером tn, с меткой l используйте вызов:

air_reservation.enter_transition (sn, tn, l)

Это включает и заключительные состояния, для которых по умолчанию tn равно 0. Затем можно запустить приложение:

air_reservation.execute_session.

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

Обсуждение

Этот пример, надеюсь, показал впечатляющую картину той разницы, которая существует между ОО-конструированием ПО и ранними подходами. В частности, он показал преимущества, получаемые при устранении понятия главной программы. Сосредоточившись на понятии абстракции данных, забывая столь долго, пока это еще возможно, о том, что является главной функцией системы, мы получаем структуру, более подготовленную к будущим изменениям и повторному использованию в разнообразных вариантах.
Этот стабилизирующий эффект является одним из характеристических свойств Метода. Он предполагает некоторую дисциплину применения, поскольку такой способ проектирования постоянно наталкивается на сопротивление и естественное желание спросить, "А что же делает система?". Это один из тех навыков, по которому можно отличить ОО-профессионала от людей, которые не проникли в суть Метода, хотя и могут использовать ОО-язык программирования и объектную технику, но за объектным фасадом их систем по-прежнему стоит функциональная архитектура.
Как показано в этой лекции, идентифицировать ключевые абстракции часто удается, анализируя передачу данных и обращая внимание на те понятия, что чаще других используются в коммуникациях между компонентами системы. Часто это является прямым указанием на обращение ситуации - присоединение программ к абстрактным данным.
Заключительный урок этой лекции состоит в том, что следует быть осторожным и не придавать слишком большого значения тому факту, что ОО-системы выведены непосредственно путем моделирования "реального мира". Моделирующая мощь метода и в самом деле впечатляющая, и вполне приятно создавать программную архитектуру, чьи принципиальные компоненты непосредственно отражают абстракции внешней моделируемой системы. Но построить модель реального мира можно разными способами, не все они приводят к хорошей программной системе. Наша первая, goto версия была также близка к реальному миру, как и две другие, - фактически даже ближе, поскольку ее структура была построена по образцу диаграммы переходов системы, в то время как другие версии вводили промежуточные понятия. Но результат с точки зрения программной инженерии является катастрофическим.
Созданная в конечном итоге ОО-декомпозиция хороша из-за использования абстракций: STATE, APPLICATION, ANSWER - все они являются ясными, общими, управляемыми, готовыми к изменениям и повторному использованию в широкой области применения. Вы понимаете, что эти абстракции столь же реальны, как и все остальное, но новичку они могут казаться менее естественными, чем концепции, используемые в ранее изучаемых решениях.
При создании хорошего ПО следует учитывать не его близость к реальному миру, а то, насколько выбранные абстракции хороши как для моделирования внешней системы, так и для построения структуры ПО. Фактически в этом суть ОО-анализа, проектирования и реализации - работа, которую для успеха проекта необходимо выполнять хорошо как сегодня, так и завтра. Профессионала от любителя отличает умение находить правильные абстракции.

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика