![]() |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Образец проектирования: многопанельные интерактивные системы Многопанельные системы
Первая напрашивающаяся попытка
Чтобы получить не просто систему, а хорошую систему, придется еще поработать. Функциональное решение: проектирование сверху внизПовторяя на этом частном примере эволюцию, пройденную программированием в целом, перейдем от низкоуровневого подхода goto к иерархической структуре, создаваемой при проектировании сверху вниз. Это решение принадлежит общему стилю, известному как "структурное", хотя этот термин следует использовать с осторожностью. Функция переходовПервым шагом на пути улучшения нашего решения будет придание центральной роли алгоритму обхода в структуре ПО. Диаграмма переходов - это всего лишь одно из свойств нашей системы, и нет никаких оснований, чтобы она правила над всеми частями системы. Ее отделение от остального алгоритма позволит, по крайней мере, избавиться от goto. Можно ожидать и большей общности, поскольку диаграмма переходов специфична для приложения, такого как резервирование авиабилетов, в то время как алгоритм обхода графа более универсален.
Архитектура программыСледуя традиционным рекомендациям декомпозиции сверху вниз, выберем "вершину" - главную функцию нашей системы. Это должна быть, очевидно, программа execute_session, описывающая выполнение полной интерактивной сессии. 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 (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 фактически во многом подобен классу, даже если остальная структура не является объектной.)
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||
Закон инверсии Если программы пересылают друг другу много данных, поместите программы в данные. |
Ранее модули строились вокруг операций (таких как 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:
Все компоненты функциональной декомпозиции нашли свое место в ОО-декомпозиции: одни в классе STATE, другие в APPLICATION. Это не должно нас удивлять. Объектная технология, о чем многократно говорится в этой книге, является прежде всего архитектурным механизмом, в первую очередь предназначенным для организации программных элементов в согласованные структуры. Сами элементы, возможно, нижнего уровня, те же самые или похожие на элементы необъектных решений. Объектные механизмы: абстракция данных, скрытие информации, утверждения, наследование, полиморфизм, динамическое связывание позволяют сделать задачу проектирования более простой, общей и мощной.
Системе управления панелями, изучаемой в данной лекции, всегда необходимы: процедура обхода графа приложения (execute_session, теперь просто execute), чтение ввода пользователя (read), обнаружение заключительного состояния (is_final). Погружаясь в структуру, можно найти одни и те же элементы, независимо от выбранного подхода к проектированию. Что же меняется? - способ группирования элементов, создающий модульную архитектуру.
Конечно, нет необходимости ограничивать себя элементами, пришедшими из предыдущих решений. То, что для функционального решения было завершением процесса - построение функции execute и всего необходимого для ее работы - теперь становится только началом. Существует много других вещей, которые хотелось бы выполнять для подобных приложений:
Все эти операции и другие будут в равной степени являться компонентами класса APPLICATION. Здесь нет более или менее важных программ, чем наша бывшая "главная программа", - процедура execute, ставшая теперь обычным компонентом класса, равная среди других, но не первая. Устраняя понятие вершины, мы подготавливаем систему к эволюции и повторному использованию.
Для завершения рассмотрения класса APPLICATION рассмотрим несколько возможных реализационных решений:
Вот определение класса, использующего эти решения:
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 - все они являются ясными, общими, управляемыми, готовыми к изменениям и повторному использованию в широкой области применения. Вы понимаете, что эти абстракции столь же реальны, как и все остальное, но новичку они могут казаться менее естественными, чем концепции, используемые в ранее изучаемых решениях.
При создании хорошего ПО следует учитывать не его близость к реальному миру, а то, насколько выбранные абстракции хороши как для моделирования внешней системы, так и для построения структуры ПО. Фактически в этом суть ОО-анализа, проектирования и реализации - работа, которую для успеха проекта необходимо выполнять хорошо как сегодня, так и завтра. Профессионала от любителя отличает умение находить правильные абстракции.