Диаграммы состояний

Диаграммы состояний в UML являются реализацией основной идеи использования конечных автоматов как средства описания алгоритмов и, тем самым, моделирования поведения. Конечные автоматы в UML реализованы довольно своеобразно. С одной стороны, в основу положено классическое представление автомата в форме графа состояний-переходов. С другой стороны, к классической форме добавлено большое число различных расширений и вспомогательных обозначений, которые, строго говоря, не обязательны, но весьма удобны и наглядны при составлении диаграмм. В результате диаграммы состояний UML более наглядны и выразительные по сравнению с классическими представлениями автоматов, но их применение требует большей подготовленности пользователя и предъявляет более высокие требования к "сообразительности" и "внимательности" инструментов моделирования.

Начиная обзор средств моделирования с самого верхнего уровня, можно констатировать, что на диаграммах состояний применяется всего один тип сущностей — состояния, и всего один тип отношений — переходы. Совокупность состояний и переходов между ними образует машину состояний. На рисунке 4.1 приведена метамодель машины состояний UML в самом общем виде.

Рисунок 4.1. Метамодель машины состояний

Мы видим, что автомат (машина состояний) состоит из набора состояний, для каждого из которых определены исходящие (outgoing) переходы и входящие (incoming) переходы, и набора переходов, для каждого из которых определено исходное (source) и целевое (target) состояния. Для машины в целом определен контекст (context), т. е. тот элемент модели, описанием поведения которого она является. Кроме того, имеется ссылка на одно из составных состояний (атрибут top), смысл и назначение которой обсуждается ниже.

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

Состояния бывают:

• простые,

• составные,

• специальные

и каждый тип состояний имеет дополнительные подтипы и различные составляющие элементы.

Переходы бывают простые и составные, и каждый переход содержит от двух до пяти составляющих:

• исходное состояние,

• событие перехода,

• сторожевое условие,

• действие на переходе,

• целевое состояние.

Рассмотрим все эти элементы по порядку.

Простое состояние имеет следующую структуру:

• имя;

• действие при входе;

• действие при выходе;

• множество внутренних переходов;

• внутренняя активность;

• множество отложенных событий.

Имя состояния является обязательным.[12] Все остальные составляющие простого состояния не являются обязательными. Фактически, имя (простого) состояния — это символ алфавита состояний Q.

Действие при входе (обозначается при помощи ключевого слова entry) — это указание атомарного действия, которое должно выполняться при переходе автомата в данное состояние. Действие при входе выполняется после всех других действий, предписанных переходом, переводящим автомат в данное состояние.

Действие при выходе (обозначается при помощи ключевого слова exit) — это указание атомарного действия, которое должно выполняться при переходе автомата из данного состояние. Действие при выходе выполняется до всех других действий, предписанных переходом, выводящим автомат из данного состояния. Множество внутренних переходов — это множество простых переходов из данного состояния в это же состояние (так называемых переходов в себя). Внутренний переход отличается от простого перехода в себя тем, что действия при выходе и входе не выполняются. Синтаксис внутреннего перехода совпадает с синтаксисом простого перехода.

Внутренняя активность (обозначается при помощи ключевого слова do) — это указание деятельности, которая начинает выполняться при переходе в данное состояние после выполнения всех действий, предписанных переходом, включая действие на входе. Внутренняя активность либо заканчивается по завершении, либо прерывается в случае выполнения перехода (в том числе и внутреннего перехода). В классической модели конечный автомат, находясь в некотором состоянии, ничего не делает: он находится в состоянии ожидания перехода. В модели UML считается, что автомат можно нагрузить какой-то полезной фоновой деятельностью, которая будет прерываться при выполнении любого перехода.

Если в то время, когда автомат находится в некотором состоянии, происходит событие, для которого в данном состоянии не определен переход, то согласно семантики UML ничего не происходит и событие безвозвратно теряется. В некоторых случаях этого требуется избежать. Для этого в UML предусмотрено понятие отложенного события. Отложенное событие — это событие, для которого не определено перехода в данном состоянии, но которое, тем не менее, не должно быть потеряно, если оно произойдет, пока автомат находится в данном состоянии. Семантика отложенного события такова: если происходит отложенное событие, то оно помещается в конец некоторой системной очереди отложенных событий. После перехода автомата в новое состояние проверяется (начиная с начала) очередь отложенных событий. Если в очереди есть событие, для которого в новом состоянии определен переход, то событие извлекается из очереди и происходит переход.

Рассмотрим пример из информационной системы отдела кадров. Проектируя жизненный цикл сотрудника, ограничимся пока одним простым состоянием — Working, соответствующему ситуации, когда сотрудник принят на работу и работает. С помощью действий на входе и выходе мы отмечаем необходимость выполнения соответствующих манипуляций с учетной записью сотрудника. Далее, мы можем отметить, что существует событие — командировка, которое не выводит сотрудника из данного состояния, но требует выполнения каких-то действий. Мы можем различать длительные и местные командировки: скажем, при местной командировке сотрудник только должен временно передать текущие тела кому-то, а при длительной командировке нужно исключить его из исполнителей проектов и заблокировать учетную запись. Различие в деталях реакций на эти события можно передать с помощью перехода в себя и внутреннего перехода. Если ничего не происходит, то по умолчанию сотрудник должен выполнять свои основные обязанности. Соответствующее простое состояние приведено на рисунке 4.2.

Рисунок 4.2. Простое состояние сотрудника

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

Прочие составляющие — событие перехода, сторожевое условие и действия на переходе не являются обязательными. Если они присутствуют, то изображаются в виде текста в определенном синтаксисе рядом со стрелкой, изображающей переход. Синтаксис описания перехода следующий:

Событие [ Сторожевое условие ] / Действие

Например, в информационной системе отдела кадров переход сотрудника из состояния "кандидат" в состояние "в штате" может быть описано с помощью простого перехода, представленного на рисунке 4.3.


Рисунок 4.3. Простой переход

Общая семантика перехода такова. Допустим, что автомат находится в состоянии S1, в котором определен исходящий переход T с событием Е, сторожевым условием G и действием D, ведущий в состояние S2. Если возникает событие E, то переход T возбуждается (в данном состоянии могут быть одновременно возбуждены несколько переходов — это не считается противоречием в модели). Далее проверяется сторожевое условие G (проверяются сторожевые условия всех возбужденных переходов в неопределенном порядке). Если сторожевое условие выполнено, то переход T срабатывает — выполняется действие на выходе из состояния S1, выполняется действие на переходе D, выполняется действие на входе в состояние S2 и автомат переходит в состояние S2. Даже если у нескольких возбужденных переходов сторожевые условия оказываются истинными, то, тем не менее, срабатывает всегда только один переход из возбужденных. Какой именно переход срабатывает — не определено. Таким образом поведение, описываемое подобным автоматом, является недетерминированным.[13] Если же сторожевое условие G не выполнено, то переход T не срабатывает. Если ни один из возбужденных переходов не срабатывает, то событие T теряется и автомат остается в состоянии S1.

Событие перехода — это тот входной символ (стимул), который вкупе с текущим состоянием автомата определяет следующее состояние.

В UML предусматривается несколько типов событий. Само слово "событие" невольно вызывает следующую ассоциацию: существует некий внешний по отношению к автомату мир, в котором время от времени происходят события (в общечеловеческом смысле этого слова), автомату становится известно о произошедшем событии и он реагирует на событие путем перехода в определенное состояние. Эта ассоциация вполне правомерна, если речь идет о моделировании жизненного цикла объекта в программе, управляемой событиями. Действительно: в этом случае основной тип событий — это события вызова методов объекта. Объект реагирует на них, выполняя тела методов и меняя значения своих атрибутов (состояние). Однако, данная ассоциация далеко не единственно возможная (хотя и самая распространенная в объектно-ориентированном программировании). Автомату не важен источник событий: важна последовательность, в которой события поступают на вход автомата, вынуждая его реагировать. Например, последовательность символов в слове входного алфавита A также рассматривается как последовательность событий. Для автомата существенно, что первое событие предшествует второму, а почему это происходит: потому ли что первое событие произошло раньше или же потому что расположено левее — это не важно. Таким образом, конечные автоматы, в том числе машины состояний UML, подходят для моделирования поведения не только событийно-управляемых программ, но и любого другого типа управления.

UML допускает наличие переходов без событий — такой переход называется переходом по завершении.

Переход по завершении - это переход без событий.

Семантика перехода по завершении такова. Имеется одно неявное, а потому безымянное событие, которое наступает при завершении внутренней активности в состоянии (если никакой внутренней активности не предусмотрено, то это событие наступает немедленно после перехода автомата в данное состояние). Поскольку событие завершения является безымянным, оно никак не отображается на диаграмме: в тексте, сопутствующем переходу, просто отсутствует первая часть, относящаяся к событию перехода. В остальном переходы по завершении ничем не отличаются от простых переходов: они могут содержать сторожевые условия, может быть несколько исходящих переходов по завершении для данного состояния (с альтернативными сторожевыми условиями), для переходов по завершении можно определять последовательности действий при переходе и т. д.

Следует, однако, иметь в виду, что при частом использовании переходов по завершении легче допустить и труднее обнаружить ошибку модели, порождающую неопределенное поведение. Простой пример: допустим, что имеется состояние, для которого определено несколько исходящих переходов по завершении (с разными сторожевыми условиями) и не определено других переходов по событиям. Допустим, что ни один из переходов по завершении не сработает, ввиду того, что все сторожевые условия окажутся ложными. В таком случае, поскольку событие завершения возникает ровно один раз и оно уже утеряно, а все другие события в данном состоянии теряются, так как для них не определены переходы, моделируемая система будет демонстрировать характерное поведение, печально знакомое слишком многим пользователям: "зависание". Таким образом, переход по завершении средство удобное, но опасное. В классические модели конечных автоматов подобные средства обычно не включают, благодаря чему становится возможна автоматическая проверка отсутствия "зависаний" и других неприятностей. Мы настойчиво рекомендуем не использовать переходы по завершении в машинах состояний, кроме случаев крайней необходимости, примеры которых приведены ниже. Другое дело диаграммы деятельности — там переходы по завершении являются основным средством.

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

Для каждого возбужденного перехода сторожевое условие проверяется ровно один раз, сразу после того, как переход возбужден и до того, как в системе произойдут какие-либо другие события. Если сторожевое условие ложно, то переход не срабатывает и событие теряется. Даже если впоследствии сторожевое условие станет истинным, переход сможет сработать, только если повторно возникнет событие перехода.

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

В UML предусмотрены синтаксические средства, до некоторой степени облегчающие семантически правильное построение сторожевых условий за счет более наглядного их изображения. Таковыми являются:

• сегментированные переходы;

• символы ветвления;

• переходные состояния;

• предикат else.

Линия перехода может быть разбита на части, называемые сегментами.

Сегменты перехода — части, на которые может быть разбита линия перехода.

Разбивающими элементами являются следующие фигуры:

• переходное состояние (изображается в виде небольшого кружка);

• ветвление (изображается в виде ромба);

• действия посылки и приема сигнала.

Сегментирование перехода и переходные состояния применяются в UML в нескольких ситуациях. Здесь мы рассматриваем их в связи со сторожевыми условиями, а прочие случаи описаны в соответствующем контексте.

Несколько переходов, исходящих из данного состояния и имеющих общее событие перехода, можно объединить в дерево сегментированных переходов следующим образом. Имеется один сегмент перехода, который начинается в исходном состоянии и заканчивается в переходном состоянии или ветвлении. Далее из этого переходного состояния или ветвления начинаются другие сегменты, которые заканчиваются в целевых состояниях или новых переходных состояниях или ветвлениях. Сегмент перехода, начинающийся в исходном состоянии, называется корневым, сегменты, заканчивающиеся в целевых состояниях, называются листовыми. Событие перехода может быть указано только для корневого сегмента, действия на переходе могут быть указаны только для листовых сегментов, а сторожевые условия могут быть указаны для любых сегментов. Такое дерево сегментированных переходов семантически эквивалентно множеству простых переходов, которое получится, если рассмотреть все пути из исходного состояния в целевые состояния, считая встречающиеся на пути сторожевые условия соединенными конъюнкцией.

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

• non grata — скандалист, бездельник и нарушитель трудовой дисциплины, уволенный по инициативе администрации, которого ни при каких обстоятельствах нельзя нанимать на работу;

• welcome — хороший работник, с котором администрации пришлось расстаться ввиду временных трудностей, переживаемых предприятием, и которого при первой возможности следует пригласить обратно;

• retired — работник уволившийся по собственному желанию, повторный прием которого должен проходить на общих основаниях.

Это, разумеется, очень грубая классификация, но для демонстрационных целей вполне достаточная. На рисунке 4.4 представлен соответствующий фрагмент диаграммы состояний, в котором использованы дерево сегментированных переходов и переходные состояния.

Рисунок 4.4. Дерево сегментированных переходов

Переходные состояния в данном контексте имеют тот же смысл и могут быть использованы вместо фигур ветвления с сохранением семантики диаграммы (Рисунок 4.5).

Продолжим рассмотрение данного примера. Мы знаем, что условия увольнения adm и self взаимно исключают друг друга и одно из них при увольнении обязательно имеет место. Но это знаем только мы — инструмент моделирования этого не знает и проверить полноту и дизъюнктность системы условий не сможет. Но он может помочь обозначить наше желание наделить систему условий нужными свойствами. Для этого используется ключевое слово else, которое обозначает условие, считающееся истинным во всех случаях, когда ложны все другие условия, приписанные к сегментам, исходящим из данного ветвления. Далее, поскольку такой предикат единственен, его можно не писать, подразумевая по умолчанию.

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

Рисунок 4.5. Использование предиката else

Подчеркнем, что сегментированные переходы и ветвления ничего не добавляют (и не убавляют) в семантике модели: это просто синтаксические обозначения, введенные для удобства и наглядности. Ту же самую семантику, которую имеют фрагменты диаграмм состояний на рисунке 4.4 и 4.5 можно передать с помощью фрагмента, приведенного на рисунке 4.6.

Рисунок 4.6. Множество простых переходов с одним событием перехода и различными сторожевыми условиями

Последней составляющей простого перехода является действие.

Действие — это непрерываемое извне атомарное вычисление, чье время выполнения пренебрежимо мало.

Авторы языка подразумевали, что инструменты моделирования будут связывать с понятием действия в модели UML понятие действия (или аналогичное) в целевом языке программирования. Например, для обычных языков программирования действиями являются вычисление значения выражения и присваивание его переменной, вызов процедуры, посылка сигнала и т. д. В UML предусмотрено несколько типов действий, похожих по семантике на действия в наиболее распространенных языках программирования. Однако UML не является языком программирования и, тем самым, не претендует на то, чтобы быть универсальным языком описания действий. Поэтому понятие действия в UML сознательно недоопределено — оставлена свобода, необходимая инструментам для непротиворечивого расширения семантики действий UML до семантики действий конкретного языка программирования. Здесь, в контексте обсуждения машины состояний UML, стоит подчеркнуть три обстоятельства.

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

• Действие является безальтернативным и завершаемым. Раз начавшись, действие выполняется до конца. Оно не может "раздумать" выполняться или выполняться неопределенно долго.

• Последовательность действий также является действием. (Синтаксически, действия в последовательности разделяются запятыми).

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


Закончим раздел, посвященный переходам, соответствующим фрагментом метамодели (Рисунок 4.7). Метамодель получилась очень простой, это объясняется тем, что метамодель отражает только абстрактный синтаксис UML, необходимый для описания семантики. Многочисленные детали способов изображения простых переходов нет нужды отражать в метамодели.


Рисунок 4.7. Метамодель простого перехода

Рассмотрим другие понятия, близкие к понятию состояния, но специфические для UML. Таковых две основные группы - составные состояния и специальные состояния.

Составное состояние может быть

• последовательным или

• параллельным (ортогональным).

Специальные состояния, в свою очередь, бывают следующих типов:

• начальное состояние,

• заключительное состояние,

• переходное состояние,

• историческое состояние (в двух вариантах),

• синхронизирующее состояние,

• ссылочное состояние,

• состояние "заглушка".

Начнем с составных состояний.

Составное состояние — это состояние, в которое вложена машина состояний. Если вложена только одна машина, то состояние называется последовательным, если несколько — параллельным.[14] Глубина вложенности в UML неограниченна, т. е. состояния вложенной машины состояний также могут быть составными.

Мы начнем с простого примера, чтобы сразу пояснить прагматику составного состояния, т. е. зачем это понятие введено в UML, а затем опишем тонкости семантики и связь с другими понятиями машины состояний UML.

Рассмотрим все известный прибор: светофор. Он может находится в двух основных состояниях:

• Off — вообще не работает — выключен или сломался, как слишком часто бывает;

• On — работает.

Но работать светофор может по-разному:

• Blinking — мигающий желтый, дорожное движение не регулируется;

• Working — работает по-настоящему и регулирует движение.

В последнем случае у светофора есть 4 видимых состояния, являющихся предписывающими сигналами для участников дорожного движения:

Green — зеленый свет, движение разрешено;

GreenYellow — состояние перехода из режима разрешения в режим запрещения движения (это настоящее состояние, светофор находится в нем заметное время);

Red — красный свет, движение запрещено;

RedYellow — состояние перехода из режима запрещения в режим разрешения движения (это состояние отличное от GreenYellow, светофор подает несколько иные световые сигналы и участники движения обязаны по другому на них реагировать).

На рисунке 4.8 приведена соответствующая диаграмма состояний (несколько забегая вперед, мы использовали здесь событие таймера, выделяемое ключевым словом after и описанное далее).

Рисунок 4.8. Составные состояния

Всегда можно обойтись без использования составных состояний. Например, на рисунке 4.9 приведена эквивалентная машина состояний (т. е. описывающая то же самое поведение), не содержащая составных состояний. Сравнение диаграмм на рисунке 4.8 и 4.9 является достаточным объяснением того, зачем в UML введены составные состояния.


Рисунок 4.9. Эквивалентная диаграмма, не содержащая составных состояний

Теперь внимательно разберемся с деталями составных состояний и связанных с ними переходов. Мы сделаем это постепенно, несколько раз возвратившись к описанию семантики составных состояний и переходов в подходящем контексте.

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

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

Начальное состояние — это специальное состояние, соответствующее ситуации, когда машина состояний еще не работает.

На диаграмме начальное состояние изображается в виде закрашенного кружка. Начальное состояние не имеет таких составляющих, как действия на входе, выходе и внутренняя активность, но оно обязано иметь исходящий переход, ведущий в то состояние, которое будет являться по настоящему первым состоянием при работе машины состояний. Исходящий переход из начального состояния не может иметь события перехода, но может иметь сторожевое условие. В последнем случае должны быть определены несколько переходов из начального состояния, причем один из них обязательно должен срабатывать. В программистских терминах начальное состояние — это метка точки входа в программу. Управление не может задержаться на этой метке. Даже графически типичный случай начального состояния с одним непомеченным переходом очень похож на бытовую пиктограмму "начинать здесь". Начальное состояние может иметь действие на переходе — это действие выполняется до начала работы машины состояний. Насколько обязательным является использование начального состояния в диаграмме состояний? Этот вопрос не имеет однозначного ответа — все зависит от ситуации. Например, в диаграммах на рисунке 4.8 и 4.9 мы обошлись без начального состояния, и поведение светофора не стало от этого менее понятным. Однако в других случаях наличие начального состояния может быть желательно или даже необходимо. Прежде всего, если имеется переход в составное состояние, то внутри этого составного состояния обязано присутствовать начальное состояние — в противном случае неясно, куда же ведет данный переход. Далее, если машина состояний описывает поведение программного объекта, создаваемого и уничтожаемого в программе, то присутствие начального состояния на диаграмме является весьма желательным: начальное состояние показывает, в каком состоянии находится объект при создании его конструктором (а в действия на переходе из начального состояния удобно поместить инициализацию атрибутов). С другой стороны, если начало и окончание жизненного цикла объекта, поведение которого моделируется, выходят за пределы моделируемого периода (например, нас не интересует ни процесс изготовления новых светофоров, ни процесс утилизации отслуживших свое), то начальное состояние на диаграмме состояний является излишним и может даже мешать восприятию.

Заключительное состояние — это специальное состояние, соответствующее ситуации, когда машина состояний уже не работает.

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

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

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

• если имеется входящий переход в составное состояние, то машина состояний, вложенная в данное составное состояние, обязана иметь начальное состояние;

• если машина состояний, вложенная в составное состояние, имеет заключительное состояние, то данное составное состояние обязано иметь исходящий переход по завершении;

• машина состояний верхнего уровня считается вложенной в составное состояние, которое не имеет ни исходящих, ни входящих переходов.

Семантику составных состояний и переходов мы хотим пояснить с помощью указания эквивалентных конструкций, не использующих составных состояний. В табл. 10 состояние A — составное, а состояния B и C — любые, т. е. простые или составные. В последнем случае правила табл. 10 рекурсивно применяются к вложенным состояниям на всю глубину вложенности.

Таблица 10.Эквивалентные выражения для переходов между составными состояниями

A
Историческое состояние — это специальное состояние, подобное начальному состоянию, но обладающее дополнительной семантикой.

Историческое состояние может использоваться во вложенной машине состояний внутри составного состояния. При первом запуске машины состояний историческое состояние означает в точности тоже, что и начальное: оно указывает на состояние, в котором находится машина в начале работы. Если в данной машине состояний используется историческое состояние, то при выходе из объемлющего составного состояния запоминается то состояние, в котором находилась вложенная машина при выходе. При повторном входе в данное составное состояние в качестве текущего состояния восстанавливается то состояние, в котором машина находилась при выходе. Проще говоря, историческое состояние заставляет машину помнить, в каком состоянии ее прервали прошлый раз и "продолжать начатое".

Историческое состояние имеет две разновидности.

Поверхностное историческое состояние запоминает, какое состояние было активным на том же уровне вложенности, на каком находится само историческое состояние.

Глубинное историческое состояние помнит не только активное состояние на данном уровне, но и на вложенных уровнях.

Рассмотрим пример из информационной системы отдела кадров. Работающий сотрудник, если все идет хорошо, пребывает в одном из двух взаимоисключающих состояний: либо он на работе (inOffice), либо в отпуске (onVacations). Но может случиться такая неприятность, как болезнь (Ill). Понятно, что заболевший сотрудник не на работе, но он и не в отпуске, болезнь — это еще одно состояние. Но в какое состояние переходит сотрудник по выздоровлении? Допустим, что в нашей информационной системе отдела кадров действует положение старого Комплекса законов о труде — если сотрудник заболел, находясь в отпуске, то отпуск прерывается, а по выздоровлении возобновляется. Для того, чтобы построить модель такого поведения, нужно воспользоваться историческим состоянием. В данном случае достаточно поверхностного исторического состояния, поскольку на данном уровне вложенности все состояния уже простые. На рисунке 4.10 приведен соответствующий фрагмент машины состояний.

Рисунок 4.10. Историческое состояние

Для тех примеров, которые мы пока что рассматривали, никаких других средств описания не нужно. Но что будет, если в диаграмме состояний сотня состояний и десяток уровней вложенности? Они просто не поместятся на диаграмме нормальных размеров, а если поместятся, то их будет невозможно прочитать и понять. Если мы хотим описать на UML действительно сложное поведение, то мы должны иметь возможность сделать это по частям, используя принцип "разделяй и властвуй". И такая возможность в UML предусмотрена — это ссылочные состояния и состояния заглушки.

Всякая машина состояний UML вложена в некоторое составное состояние (машина верхнего уровня вложена в искусственное составное состояние и именем top). Теперь можно объяснить, для чего это сделано — чтобы можно было сослаться на любую машину состояний по имени (конкретно, по имени составного состояния, в которое вложена машина).

Ссылочное состояние — состояние, которое обозначает вложенную в него машину состояний.

На диаграмме ссылочное состояние изображается в виде фигуры простого состояния с именем, которому предшествует ключевое слово include. Семантика ссылочного состояния заключается в следующем. Если на диаграмме присутствует ссылочное состояние, то это означает, что в модели вместо ссылочного состояния присутствует составное состояние (и, соответственно, вложенная машина состояний), на которое делается ссылка. Таким образом, на одной диаграмме мы можем представить поведение, нарисовав его "крупными мазками", т. е. в терминах составных состояний верхнего уровня, а на других диаграммах раскрыть содержание составных состояний, нарисовав соответствующие машины состояний и т. д. Критерий Дейкстры хорошего стиля программирования — "правильно написанный модуль должен помещаться на одной странице" — соблюдается. Переходы между составными состояниями в UML можно подразделить на два типа: переходы внутрь и изнутри составных состояний (т. е. переходы, пересекающие границы составных состояний), и переходы, начинающиеся и заканчивающиеся на границах состояний. Вообще говоря, без переходов, пересекающих границы состояний, можно обойтись (равно как и без многого другого), но жалко — это удобная возможность. Если переход начинается и заканчивается на границе состояний, то вся работа вложенной машины состояний инкасулирована в составном состоянии, семантика переходов определена в табл. 4.3 и никаких проблем нет — ссылочного состояния достаточно для включения одной машины внутрь другой. Однако, если такие переходы есть (а мы договорились, что не хотим от них отказываться), то возникает проблема: грубо говоря, нам нужно провести стрелку с одной диаграммы на другую. Решением этой проблемы в UML являются состояния заглушки.

Состояние заглушка — это специальное состояние, которое обозначает в ссылочном состоянии некоторое вложенное состояние того составного состояния, на которое делается ссылка.

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

Приведем пример из информационной системы отдела кадров, чтобы проиллюстрировать приведенные определения. В этом примере мы рассматриваем жизненный цикл сотрудника на предприятии, раскрывая детали поведения объекта Person, находящегося в самом важном для предприятия состоянии — Employee. В предыдущих примерах мы раскрывали разные детали машины состояний сотрудника. Здесь мы покажем, как с помощью ссылочного состояния можно собрать все заготовки в цельную и полную модель поведения сотрудника. На рисунке 4.11 приведена диаграмма верхнего уровня, описывающая поведение в целом, без деталей. Состояние Employee — ссылочное, что подразумевает наличие другой диаграммы, на которой раскрыта внутренняя структура состояния Employee. Состояние заглушка inOffice указывает, что внутри составного состояния Employee есть вложенное состояние inOffice. Данное состояние выявлено на диаграмме верхнего уровня, чтобы подчеркнуть, что переходы по событию fire ведут именно в это вложенное состояние, а не в другое (при приеме сотрудник сначала обязательно должен попасть в состояние "на работе" и только потом может уйти в отпуск, заболеть и т. д.).

Рисунок 4.11. Ссылочное состояние и состояние заглушка

На другой диаграмме (Рисунок 4.12) раскрывается внутренняя структура состояния Employee.

Рисунок 4.12. Составное состояние, раскрывающее ссылочное состояние

Здесь мы сразу показали все вложенные состояния Employee, как простые, так и составные, поскольку диаграмма получается достаточно компактной и обозримой. Но если бы нам нужно было учесть больше деталей поведения, например, командировки, особые режимы работы (скажем, работу в праздничные дни), работу по графику (сутки работы — трое выходных), различные типы отпусков (очередной, за свой счет, по уходу за ребенком, учебный), короче говоря, если бы все нужные детали перестали помещаться на диаграмму, то нужно было бы ввести еще несколько ссылочных состояний и раскрыть их детали на отдельных диаграммах.[15]

Для возможности практического использования метода пошагового уточнения при описании сложного поведения с помощью машин состояний в UML 2.0 явным образом введено понятие вложенной машины состояний вместо понятия ссылочного состояния и заглушки.

На диаграмме верхнего уровня указывается вложенная машина состояний как простое состояние, а на диаграмме нижнего уровня вложенная машина состояний раскрывается как составное состояние. Вместо понятия заглушки указываюися точки входа и выхода на границе вложенной машины состояний. Уточненное в UML 2.0 понятие вложенной машины состяний представляется нам настолько простым и естественным, что вместо детальных пояснений мы просто сошлемся на рисунке 4.13 и 4.14, где представлен еще один пример.

Рисунок 4.13. Составное состояние, использующее вложенную машину состояний

Рисунок 4.14. Составное состояние, раскрывающее вложенную машину состояний

В заключение приводим метамодель состояний UML (Рисунок 4.15), на которой показаны (с некоторыми упрощениями), рассмотренные в этом разделе понятия.

Рисунок 4.15. Метамодель состояния

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

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

В UML используются четыре типа событий:

• событие вызова,

• событие сигнала,

• событие таймера,

• событие изменения.

Событие вызова — это событие, возникающее при вызове операции класса.

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

Допустим, что некоторая операция класса реализована (то есть соответствующее данной операции поведение описано) с помощью события вызова машины состояний данного класса. Пусть данная операция вызвана. Тогда возможны два варианта: либо соответствующий переход срабатывает (т. е. в активном состоянии определен переход с данным событием вызова и сторожевое условие выполнено), либо не срабатывает. В каждом случае поведение зависит от того, возвращает или не возвращает значение вызываемая операция. Если операция не возвращает значения, то ошибки нет в любом случае: если переход срабатывает, то выполняются действия на переходе, автомат переходит в новое состояние и в этом заключается выполнение вызванной операции; если же переход не срабатывает, то ничего не происходит и управление немедленно возвращается в место вызова, причем это не считается ошибкой. Если же данная операция возвращает значение, то если переход не срабатывает или если переход срабатывает, но на переходе не возвращается значение или возвращается значение неподходящего типа, то это считается ошибкой, поскольку поведение, описываемое машиной состояний, не соответствует спецификации операции.

Рассмотрим пример из информационной системы отдела кадров. Допустим, что в классе Person определена операция move(newPos:Position):Boolean, которая переводит работающего сотрудника на новую должность newPos и возвращает значение true, если перевод успешно произведен. Пусть в классе Position определены операции isFree():Boolean, которая проверяет, свободна ли должность и occupy(person:Person) — назначает сотрудника на должность. Тогда типичное поведение операции move можно описать диаграммой состояний, приведенной на рисунке 4.16.

Рисунок 4.16. Событие вызова

Однако такое описание поведения, хотя и правильно определяет, что делает операция move если "все хорошо", но не является достаточно надежным. Действительно, если операция move будет вызвана в тот момент, когда сотрудник находится в состоянии Candidate или Retired, или же если должность newPos окажется занятой, то возникнет ошибка, поскольку требуемое по спецификации операции move логическое значение не будет возвращено. Диаграмма состояний на рисунке 4.17 описывает поведение операции move более надежно, поскольку обеспечивает возврат значения и в том случае, когда "не все хорошо".

Рисунок 4.17. Событие вызова с учетом всех вариантов поведения

Помимо иллюстрации понятия события вызова в UML последний пример может послужить поводом для обсуждения понятий предусловия и постусловия операций в UML.

Предусловие — это условие, налагаемое на входные данные операции, которое должно выполняться до вызова операции.

Если предусловие операции нарушено, то корректное выполнение операции не гарантируется.

Постусловие — это условие, связывающее входные и выходные данные операции, которое должно выполняться после вызова операции.

Если постусловие операции оказывается нарушенным, то значит реализация операции содержит ошибку. В UML предусловия и постусловия операций предлагается описывать при моделировании структуры, в виде ограничений со стереотипами «precondition» и «postcondition», соответственно. По сути, однако, предусловия и постусловия операций следует отнести к моделированию поведения. Впрочем, как мы уже не раз отмечали, само деление моделирования на структурное и поведенческое носит условный характер: моделирование — это единый итеративный процесс.

Определение предусловий и постусловий операций — чрезвычайно сильное средство моделирования, но рекомендации по его применению неоднозначны. Например, в только что разобранном случае операции move: условие newPos.isFree() является частью предусловия операции move или нет? Можно ответить "да" и тогда кто-то должен позаботиться о выполнении этого условия, можно ответить "нет" и тогда есть риск, что проверка не будет выполнена и в результате перевода сотрудника на занятую должность будет нарушена целостность базы данных. Чем слабее предусловие, тем шире область применения операции, она более универсальна. Но универсальность не всегда благо: иногда это никому не нужная трата ресурсов.

Событие сигнала — это событие, возникающее, когда послан сигнал.

Чтобы разъяснить событие сигнала в UML, нужно рассмотреть прежде всего саму концепцию сигнала. Здесь опять имеет место пересечение моделирования структуры и поведения: определяются сигналы в структурной части модели, а используются в поведенческой.

Синтаксически сигнал (в UML) — это объект класса со стереотипом «signal». Семантически сигнал — это именованный объект, который создается другим объектом (отправителем) и обрабатывается третьим объектом (получателем).

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

В сущности, концепция сигнала в UML принадлежит к числу наиболее фундаментальных и имеет ясную и строго определенную семантику. Объект, являющийся отправителем, обращается к классификатору сигнала (вызывает операцию send), указывая аргументы сигнала (значения атрибутов) и целевое множество объектов, которым должен быть отправлен сигнал. После этого объект- отправитель продолжает свою работу — дальнейшее его не касается. Как правило, целевое множество содержит один элемент, но может не содержать элементов (это не считается ошибкой) или может содержать много элементов (это называется широковещательным сигналом). Объекты, которым отправляется сигнал, обязаны иметь операцию для получения и обработки сигнала. Такая операция имеет стереотип «signal», ее имя совпадает с именем классификатора сигнала, а имена и типы параметров совпадают с именами и типами атрибутов сигнала. Операция получения сигнала не может возвращать результат. Классификатор сигнала создает новый объект — экземпляр сигнала и отправляет его копии всем объектам целевого множества, т. е. вызывает в объектах целевого множества операции приема сигнала с указанными значениями аргументов.

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

Сигналы подходят для описания всех видов взаимодействия объектов — как синхронных, так и асинхронных, как в монолитных приложениях, так и в распределенных. Авторы UML отмечают, что обычный вызов операции можно рассматривать как частный случай посылки сигнала: вызывающий объект отправляет сигнал вызова (с аргументами) и дожидается ответного сигнала о завершении выполнения (с возвращаемым значением). Такой подход возможен, но, на наш взгляд, понятие обычного вызова операции и так уже устоялось и можно обойтись без его сведения к более общей концепции.

В UML имеется один важный частный случай сигналов — исключения, которые определяются с помощью стереотипа «exception». Исключения во всем подобны сигналам — их назначение состоит в том, чтобы подчеркнуть в модели то обстоятельство, что для реализации должен использоваться встроенный механизм обработки исключений, который имеется в большинстве современных объектно­ориентированных систем программирования. В частности, для исключений (равно как и для сигналов) действует правило "перехвата": исключение, посланное объекту, обрабатывается операцией обработки данного исключения, определенной в ближайшем по иерархии обобщения суперклассе для класса данного объекта.

Перейдем к рассмотрению нотации, связанной с сигналами, исключениями и событиями сигнала. На диаграмме классов сигнал определяется в форме класса со стереотипом «signal» или «exception». Этот класс связывается с отправителем зависимостью со стереотипом «send» и связывается с получателем объявлением в получателе операции со стереотипом «signal».

Рассмотрим пример из информационной системы отдела кадров. Допустим, что мы решили случай занятости целевой должности при выполнении операции move перевода сотрудника обрабатывать как исключительную ситуацию. Ответственность за обработку исключительной ситуации мы решили возложить на класс Company. Такое решение на диаграмме классов можно изобразить следующим образом (Рисунок 4.18).

Рисунок 4.18. Описание сигнала на диаграмме классов

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

Рисунок 4.19. Посылка сигнала в машине состояний

В машине состояний объекта класса, ответственного за обработку исключительных ситуаций, исключение выступает как событие сигнала, возбуждающее переход. Синтаксис события сигнала ничем не отличается от события вызова (потому что обработка события сигнала это тоже выполнение операции). Мы не стали детализировать машину состояний класса Company, ограничившись одним интересным нам сейчас одним переходом с событием сигнала (Рисунок 4.20). На этом переходе выполняется не интерпретируемое действие putLog — мы полагаем необходимым в любом случае по крайней мере записать в протокол выполнения приложения факт срабатывания перехода по исключению.

Рисунок 4.20. Переход по событию сигнала

Событие таймера — это событие, которое возникает, когда истек заданный интервал времени с момента попадания автомата в данное состояние.

Синтаксически событие таймера записывается с помощью ключевого слова after, за которым указывается выражение, доставляющее длину интервала времени. Семантически событие таймера означает следующее. Подразумевается, что у состояния имеется таймер, который сбрасывается в 0, когда автомат переходит в данное состояние (напомним, что автомат считается перешедшим в состояние, когда закончено выполнение всех действий, предписанных переходом). Таймер ведет отсчет времени. Если до истечения указанного интервала времени сработает другой переход, то событие таймера не возникает. Когда указанный интервал времени истекает, наступает событие таймера и возбуждается соответствующий переход. Если переход срабатывает, то автомат переходит в новое состояние. Заметим, что если переход по событию таймера является переходом в себя, то таймер опять сбрасывается в 0, а если переход по событию таймера является внутренним переходом, то таймер продолжает отсчет. Если переход по событию таймера не срабатывает (из-за ложности сторожевого условия), то событие таймера теряется и таймер продолжает отсчет времени, так что позже может сработать другой переход по событию таймера с большим интервалом времени. Событие таймера не может быть отложено.

Мы уже привели несколько примеров использования события таймера на переходах в машинах состояний (Рисунок 4.8-4.12). Приведем еще один (Рисунок 4.21). В этом примере мы определяем, что по истечении 10 лет объект, представляющий уволенного сотрудника (запись в базе данных) уничтожается.

Рисунок 4.21. Переход по событию таймера со сторожевым условием

Событие изменения — это событие, которое возникает, когда некоторое логическое условие становится истинным, будучи до этого ложным.

Синтаксически событие изменения записывается с помощью ключевого слова when, за которым указывается логическое выражение (условие). Семантически событие изменения означает следующее. Подразумевается, что в системе имеется механизм, работающий как демон, который возбуждает событие, если в результате изменения состояния системы изменяется значение логического выражения. Если выражение, являющееся аргументом события изменения, принимает значение true (имея до этого значение false), то переход возбуждается. Если выражение имеет значение true в тот момент, когда автомат переходит в данное состояние, то переход сразу возбуждается. Если переход срабатывает, то автомат, как обычно, переходит в новое состояние. Если переход не срабатывает, то событие изменения теряется. При этом, если условие продолжает оставаться истинным, то нового события изменения не возникает. Для того, чтобы снова возникло событие изменения, нужно, чтобы условие стало сначала ложным, а потом истинным.

Рассмотрим элементарный пример из информационной системы отдела кадров: по достижении определенного возраста сотрудник увольняется на пенсию (Рисунок 4.22).

Рисунок 4.22. Переход по событию изменения

В UML 2.0 появилось еще одно важное применение аппарата машин состояний. Эта так называемая протокольная машина состояний, или протокольный автомат.

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

Смысл использования протокольного автомата состоит в следующим. Допустим, имеется объект (или компонент), который предоставляет для использования некоторый интерфейс. С точки зрения объектно-ориентированного программирования здесь должна стоять точка: мы описали набор операций, предоставляемых для использования, что еще нужно? На самом деле такого объектно-ориентированного описания интерфейса «изнутри» бывает недостаточно. Операции, предоставляемые интерфейсом, часто не являются независимыми: некоторые последовательности операций являются допустимыми, и объект должен их выполнять, но некоторые последовательности не являются допустимыми и объект не может и не должен их выполнять. Таким образом, встает задача описания допустимых и недопустимых последовательностей вызовов операций объектов, то есть описания интерфейса «снаружи». Если рассматривать вызовы операций как символы, то каждый объект определяет некоторый язык, состоящий из допустимых последовательностей символов. Конечный автомат — одно из лучших известных средств задания языков. Именно оно используется в UML под названием протокольный автомат.

Протокольный автомат ничего не делает,[16] он только проверяет допустимость последовательности операций, поэтому действия на переходах и при входе/выходе ему не нужны.

Протокольный автомат — это машина состояний, предназначенная для задания допустимых последовательностей вызовов операций и сигналов, получаемых объектом.

Рассмотрим пример. Допустим, что информационная система отдела кадров ведет учет сессий своих пользователей, например, с целью разграничения прав доступа к информации. Тогда в ней, наряду с другими операциями, должны быть предусмотрены операции login и logout, выполняемые в начале и в конце каждой сессии работы с системой (Рисунок 4.23). При этом все остальные операции (на рисунке 4.23 они условно обозначены anyOp) пользователь может выполнять только после того, как выполнена операция login и до того, как выполнена операция logout.

Рисунок 4.23. Протокольный автомат

Состав и названия состояний могут быть общими как для протокольного автомата, так и для соответствующего ему поведенческого автомата. Но в общем случае это совершенно не обязательно — у этих машин состояний разное назначение!


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: