В объектно-ориентированном мире существуют различные методы спецификации. Среди них есть полноценные языки моделирования, поддерживаемые графическими нотациями, такие как UML и BON. Кроме того, известны и чисто текстовые нотации: самостоятельные (например, Larch) или встроенные в универсальные языки программирования (такие, как Eiffel ).
Попробуем разобраться, какие языки и методы спецификации из всего этого разнообразия могут быть полезными для автоматного описания сложного поведения. В процедурном программировании с явным выделением состояний используется всего два вида спецификаций (структуры и поведения), и они позволяют достаточно полно описать систему со сложным поведением. Для спецификации структуры использовались схемы связей и диаграммы взаимодействия автоматов, а для спецификации поведения — графы переходов. Как отмечено в предыдущем разделе, при объектно-ориентированном подходе к проектированию сущностей со сложным поведением часто следует учитывать контекст — уже существующую объектно-ориентированную систему, в которую требуется внедрить новую сущность. Важно, чтобы не только модель сущности со сложным поведением, но и язык ее спецификации был согласован с существующей системой. Поэтому лучшим решением будет являться применение уже существующих и широко используемых в объектной технологии языков.Практически все существующие языки спецификации обладают развитыми средствами для описания статической модульной структуры системы: классов и отношений между ними. В наиболее богатом и популярном в настоящее время языке UML помимо средств описания структуры предлагаются также средства моделирования использования и поведения. С другой стороны, текстовые языки формальной спецификации в дополнение к структуре, как правило, обеспечивают средства декларативного описания семантики программных модулей (например, в виде контрактов). Отметим, что возможность описания семантики предусмотрена и для моделей UML: для этих целей разработан специальный объектный язык ограничений (OCL), который, однако, в настоящее время не пользуется большой популярностью. Вопросы моделирования использования выходят за рамки этой книги. Этот вид моделирования рассматривает систему с точки зрения ее пользователей. При этом сложное поведение не играет никакой роли (по крайней мере, к этому следует стремиться).
Спецификацию структуры так или иначе осуществляют все разработчики объектно-ориентированных программных систем вне зависимости от применяемого языка и метода. Не выявив классы и не задав отношения между ними, систему просто невозможно реализовать. Различие состоит в том, что для одних разработчиков процесс проектирования и описания модели представляет собой отдельную фазу создания программной системы, а для других он происходит незаметно, в уме, и результаты этого процесса не фиксируются.
Должна ли быть спецификация структуры системы текстовой или графической — это в большой степени дело вкуса. Конечно, графическое представление структуры (во всех известных авторам нотациях оно называется диаграммой классов) более наглядно. Однако пользовательский интерфейс всех существующих на сегодняшний день инструментов визуального проектирования программ [65] оставляет желать много лучшего. Поэтому визуальное конструирование модели оказывается процессом гораздо более трудным и длительным по сравнению с ее текстовым описанием. Пока инструментов с лучшим интерфейсом не существует, оптимальным представляется вариант среды разработки, которая по модели системы может строить различные ее виды (в том числе текстовые и графические), причем через любой из них можно вносить изменения в модель. В таком случае наглядность диаграммы можно совместить с удобством редактирования текстового описания, а кроме того, угодить различным вкусам разработчиков. Как изменится описание статической структуры системы с появлением в ней автоматизированных классов? Выше было неоднократно упомянуто, что внешне автоматизированные классы ничем не отличаются от других. Поэтому та часть диаграммы классов, которая описывает связи автоматизированных классов с их клиентами, никак не изменится. С другой стороны, на диаграмме классов можно особым образом отобразить взаимодействие автоматизированного класса с его объектом управления. Богатые языки спецификации, такие как UML, позволяют превратить этот участок диаграммы классов фактически в схему связей (рис. 3.5).На этой диаграмме автоматизированный класс связан с запросами и командами объекта управления с помощью ассоциаций, помеченных идентификаторами входных и выходных переменных. Компоненты самого автоматизированного класса помечены краткими идентификаторами событий. Для наглядности к классу Alarm_Clock добавлено ограничение automated. При этом из диаграммы видно, что этот класс является автоматизированным. Напомним, что краткие идентификаторы событий, входных и выходных переменных в автоматном программировании вводятся для обеспечения компактности графа переходов, и они необязательно будут фигурировать в тексте программы, особенно если этот текст генерируется по графическому описанию автоматически. На самом деле компоненты классов по правилам UML не являются сущностями и соединить их ассоциациями невозможно. Поэтому формально можно считать, что каждая ассоциация связывает два класса (автоматизированный и класс объекта управления), имеет имя, совпадающее с идентификатором входной или выходной переменной, и, кроме того, снабжена помеченным значением в виде имени соответствующего компонента объекта управления. веденной выше диаграммой классов, генератор кода сможет поместить переходы, помеченные событиями, в тела соответствующих компонентов автоматизированного класса и заменить обращения автомата к входным и выходным переменным вызовами соответствующих компонентов объекта управления. В предыдущем разделе было упомянуто, что объект управления не всегда следует выделять в отдельный класс. Это замечание актуально для часов с будильником, так как их объект управления представляет собой недостаточно четко определенную абстракцию. Возможно, лучший вариант архитектуры представлен на рис. 3.6.Здесь прямыми поставщиками часов с будильником являются классы Time (время) и Bell (звонок). Запросы объекта управления специфичны для часов с будильником и поэтому реализованы в самом автоматизированном классе. Их связь с краткими идентификаторами входных переменных, по аналогии с событиями, описана с помощью помеченных значений. Итак, схеме связей нашлось место в объектно ориентированном программировании с явным выделением состояний. Что касается диаграммы взаимодействия автоматов, то она здесь бесполезна, поскольку взаимодействие автоматизированных объектов управления можно отобразить на стандартной диаграмме классов. Перейдем к вопросам моделирования поведения, которые относятся к теме этой книги самым непосредственным образом. Здесь следует различать декларативную и императивную спецификацию поведения. Декларативная спецификация описывает поведение сущности с точки зрения ее клиентов, отвечает на вопрос «Что можно ожидать от этой сущности?» Примером такой спецификации являются контракты. Императивная спецификация, напротив, рассматривает поведение с точки зрения самой сущности и отвечает на вопрос: «Как достичь того, что ожидают клиенты?» Простейшим примером такой спецификации является текст программы на императивном языке. Однако, как читатель уже имел возможность убедиться, текстовое описание сложного поведения не обладает достаточной наглядностью. Поэтому императивная спецификация сложного поведения должна выполняться в виде графа переходов, диаграммы состояний или другой аналогичной графической нотации.Для того чтобы не нарушать модульную структуру системы, созданную при объектной декомпозиции, необходимо строить отдельную диаграмму переходов для каждой сущности со сложным поведением (каждого автоматизированного класса). Отметим, что в языке UML (по крайней мере в первой его версии) такой рекомендации по использованию диаграмм состояний дано не было, несмотря на то что язык по своей сути является объектно-ориентированным. В результате диаграммы состояний применялись для описания поведения системы в целом или произвольной ее части, что не позволяло органично встроить эти описания в общую модель системы и интегрировать с другими диаграммами. По мнению авторов, это и явилось основной причиной, по которой диаграммы состояний долгое время использовались в объектной технологии только в качестве документации, а не как полноценные формальные спецификации. При этом в результате опроса, проведенного С. Орликом (Borland), было установлено, что даже в тех случаях, когда архитекторы программной системы создают диаграммы состояний, при переходе к реализации разработчики их обычно не используют. Если следовать приведенной выше рекомендации, исчезает разница между графами переходов, диаграммами состояний и другими нотациями, поддерживающими различные расширения базовой модели конечного автомата. Поскольку объектная декомпозиция заменила автоматную, больше нет необходимости ни во вложенных и вызываемых автоматах, ни в ортогональных и суперсостояниях (хотя группировку состояний, безусловно, можно использовать как косметическое средство для улучшения внешнего вида диаграмм). Все, что требуется от графической нотации, — это состояния и переходы между ними с условиями (в виде булевых формул от событий и входных переменных) и действиями (в виде одиночных выходных переменных или их последовательностей).Поговорим немного о декларативном описании сложного поведения. Этот вопрос в настоящее время изучен гораздо меньше. Причина этого, по мнению авторов этой книги, в том, что круг исследователей, которые интересуются формальными спецификациями и контрактами, практически не пересекается с кругом тех, кто озабочен проблемами сложного поведения. Контракт является частью интерфейса класса и, как правило, предназначен для его клиентов. Если обыкновенные классы в системе снабжены контрактами, то и автоматизированные классы должны иметь контракты точно такого же вида. Однако составление содержательного контракта для сущности со сложным поведением может быть непростой задачей. Спецификации компонентов этой сущности должны отражать ее реакцию во всех возможных управляющих состояниях. Например, если вспомнить уже знакомые читателю часы с будильником, каким должно быть постусловие компонента h? Оно может быть, например, следующим: «Если часы в первом или в третьем состоянии, то увеличилось число часов текущего времени, а если во втором — то увеличилось число часов срабатывания будильника». Такой контракт очень содержателен, однако он раскрывает внутреннюю информацию об управляющих состояниях. Для клиентов класса такой контракт, скорее всего, будет являться чрезмерной спецификацией. Можно предложить другой вариант: «Увеличилось либо число часов текущего времени, либо число часов срабатывания будильника», или: «Число минут не изменилось, а число часов текущего времени и число часов срабатывания будильника не могли оба увеличиться». Будет ли такая спецификация достаточной? Что именно необходимо знать клиенту о работе компонента h? Стандартных рекомендаций по этому поводу не существует, по крайней мере, на данный момент. Каким бы ни был контракт автоматизированного класса, его поведение в каждом из управляющих состояний в отдельности является частным случаем общего, сложного поведения. Поэтому если составить контракт на поведение сущности в одном конкретном состоянии, то он должен быть сильнее, чем общий контракт на сложное поведение. Это условие аналогично принципу подстановочности, который используется при определении наследования подтипов: поскольку потомок является частным случаем предка, то спецификация потомка должна быть сильнее спецификации предка. Из этой аналогии следуют два интересных вывода. Во первых, сложное поведение в объектно-ориентированном контексте можно считать особой формой полиморфизма. Сущность со сложным поведением подобна полиморфной сущности некоторого типа, к которой во время выполнения могут быть присоединены объекты различных его подтипов (каждый подтип соответствует одному управляющему состоянию).Во-вторых, такое понимание сложного поведения приводит напрямую к образцу проектирования State, который как раз и предполагает реализацию сложного поведения с помощью классического полиморфизма подтипов. Единственное различие состоит в том, что в этом образце сложное поведение реализуется за счет порождения подтипов класса, описывающего управляющее состояние, а не саму сущность со сложным поведением. Этот дополнительный уровень косвенности необходим, так как иначе невозможно реализовать механизм переходов между состояниями. Подробнее образец проектирования State рассмотрен в следующем разделе, посвященном вопросам реализации. Вопрос о том, имеются ли противоречия между моделью автоматизированного класса и концепциями проектирования по контракту (особенно с учетом механизма наследования), до конца еще не изучен. Ряд авторов занимался вопросами наследования автоматов [67, 68], поскольку проблема повторного использования описания логики сложного поведения очень актуальна. Однако говорить о наследовании автоматов не совсем корректно, так как автомат не является самостоятельной абстракцией. Напротив, очень плодотворным полем для исследования является наследование автоматизированных классов. Подведем итоги обсуждения вопросов спецификации сущностей со сложным поведением в объектно-ориентированном контексте в виде следующей общей рекомендации. Для каждого автоматизированного класса в системе необходимо построить: описание его интерфейса (в текстовом и/или графическом виде с использованиyyем того языка, на котором описаны интерфейсы остальных классов системы);схему связей: сопоставление кратких идентификаторов событий, входных yyи выходных переменных компонентам автоматизированного класса и объекта управления (в том случае, если на диаграмме переходов используются краткие идентификаторы);диаграмму переходов управляющего автомата; спецификацию объекта управления. Такую спецификацию возможно автоматически преобразовать в текст на объектно-ориентированном языке программирования или непосредственно интерпретировать во время выполнения программной системы. Для того чтобы проиллюстрировать все основные особенности предложенной технологии проектирования и спецификации, рассмотрим несколько более сложный пример, чем те, что приводились в этой книге ранее. Пусть требуется разработать клиентскую часть системы онлайн-бронирования авиабилетов. Бронирование происходит в три этапа. Пользователь: задает набор критериев, которым должен удовлетворять рейс выбирает из списка всех рейсов, удовлетворяющих критериям, наиболее подyyходящий;вводит личные данные, для того чтобы выполнить бронирование билетов. Каждому из этих этапов в пользовательском интерфейсе системы сопоставим отдельный экран. Внешний вид экранов может быть, например, таким, как показано на рис. 3.7–3.9.Система рассчитана на постоянных пользователей. Поэтому в целях экономии времени на этапе бронирования предлагается возможность сохранения личных данных пользователя. При следующем входе пользователя в систему сохраненные значения будут использованы в качестве исходных. Если пользователь изменит личные данные на экране бронирования, то у него появится возможность либо сохранить изменения, либо отменить их, загрузив предыдущую сохраненную версию. В соответствии с предлагаемым методом проектирования проведем объектную декомпозицию. В системе можно выделить сущность Application (приложение1), которая отвечает за поведение сервиса в целом и за переходы между этапами. Выделим также сущность Reservation_Data, в которой инкапсулируем все данные о текущем заказе: идентификатор рейса, число мест, класс и личные данные пользователя. Кроме того, пусть эта сущность также осуществляет запросы к базе данных рейсов и заказов. Поскольку приложение имеет графический пользовательский интерфейс, выделим сущность GUI, которая обладает стандартной функциональностью, предоставляемой библиотекой поддержки графического интерфейса. Посредством этой сущности можно делать запросы к элементам управления и давать им команды. Кроме того, эта сущность сообщает приложению обо всех действиях пользователя. Наконец, сопоставим отдельную сущность каждому этапу процесса бронирования или экрану: Enquiry — для поиска рейсов, Choice — для выбора рейса, Reservation — для собственно бронирования. Общий взгляд на архитектуру системы отражен на диаграмме классов (рис. 3.10).*1 Разрабатываемая система является, скорее, веб-сервисом. Термин «приложение» использован как более привычный. Какие из выделенных сущностей обладают сложным поведением? Хороший кандидат — сущность Application. Она отвечает за переходы между этапами, кроме того, в зависимости от этапа нажатие одних и тех же кнопок («Вперед» и «Назад») вызывает разный эффект. Сложное поведение также наблюдается на этапе бронирования (сущность Reservation). Здесь возможность сохранить или загрузить личные данные зависит от предыстории (от того, изменял ли пользователь эти данные в текущей сессии работы с приложением). Этим двум сущностям сопоставимы автоматизированные классы, а остальным — обыкновенные.
Начнем с описания автоматизированного класса Application. Он имеет достаточно богатый интерфейс, поскольку именно этому классу сущность GUI сообщает обо всех действиях пользователя. Большинство вызовов компонентов класс Application без изменения передает на обработку текущему экрану. Реализация такого поведения тривиальна, но громоздка. Ниже при спецификации структуры и поведения будем учитывать только те компоненты класса Application, которые представляют особый интерес. Этот класс управляет переключением экранов, а также данными о заказе. Такой объект управления не является достаточно самостоятельной абстракцией. Поэтому не будем выделять его в отдельный класс. Часть команд объекта управления будет реализована непосредственно в автоматизированном классе Application, а остальные — в его классах-поставщиках. Схема связей для этого автоматизированного класса приведена на рис. 3.11, а диаграмма переходов — на рис. 3.12.На диаграммах показано, как действия пользователя транслируются классом Application в вызовы компонентов экрана Reservation: события e3–e6 в третьем состоянии напрямую сопоставляются выходным переменным z9–z12. Подобная трансляция имеет место и для других экранов, однако на диаграммах для краткости она не отображена. Перейдем к рассмотрению автоматизированного класса Reservation. Он управляет некоторыми элементами пользовательского интерфейса и личными данными пользователя. Такой объект управления также не стоит выделять в отдельный класс. Схема связей и диаграмма переходов класса Reservation изображены соответственно на рис. 3.13 и 3.14.Как мог убедиться читатель, подход к проектированию и спецификации, основанный на автоматизированных классах, позволил построить простую и элегантную архитектуру в чисто объектно-ориентированном стиле.