5.3. Диаграммы состояний и переходов
5.3. Диаграммы состояний и переходов
Существенное: состояния и переходы
Диаграмма состоянии и переходов показывает: пространство состояний данного класса; события, которые влекут переход из одного состояния в другое; действия, которые происходят при изменении состояния. Мы приспособили обозначения, использованные Харелом [11]: его работа предоставляет простой, но очень выразительный подход, который гораздо эффективнее традиционных автоматов с конечным числом состояний [Мы дополнили его работу применительно к объектно-ориентированному программированию, следуя предложениям Румбаха [12] и Беара и др. [13]]. Отдельная диаграмма состояний и переходов представляет определенный ракурс динамической модели отдельного класса или целой системы. Мы строим диаграммы состояний и переходов только для классов, поведение которых (управляемое событиями) для нас существенно. Мы можем также представить диаграмму состояний и переходов для управляемого событиями поведения системы в целом. Эти диаграммы используются в ходе анализа, чтобы показать динамику поведения системы, а в ходе проектирования - для выражения поведения отдельных классов или их взаимодействия.
Рис. 5-18. Значок состояния.
Два основных элемента диаграммы состояний и переходов - это, естественно, состояния и переходы между ними.
Состояния. Состояние представляет собой итоговый результат поведения системы. Например, только что включенный в сеть телефон находится в начальном состоянии: его предыдущее поведение несущественно, при этом он готов к тому, чтобы позвонить или принять звонок. Если кто-нибудь поднимет трубку, телефон перейдет в состояние готовности к набору номера; в этом состоянии мы не ожидаем, что телефон зазвонит, но приготовились к беседе с одним или несколькими абонентами. Если кто-либо наберет ваш номер, а телефон находится в начальном состоянии (трубка положена), то когда вы поднимете трубку, телефон перейдет в состояние с установленным соединением, и вы сможете поговорить со звонившим.
В любой момент времени состояние объекта определяет набор свойств (обычно статический) объекта и текущие (обычно динамические) значения этих свойств. Под "свойствами" подразумевается совокупность всех связей и атрибутов объекта. Мы можем обобщить понятие состояния так, чтобы оно было применимо и к объекту, и к классу, так как все объекты одного класса "живут" в одном пространстве состояний. Это пространство может представлять собой неопределенное, хотя конечное множество возможных (но не всегда ожидаемых или желаемых) состояний. На рис. 5-18 показано обозначение, которое мы используем для отдельного состояния.
Каждое состояние должно иметь имя; если оно оказывается слишком длинным, то его можно сократить или увеличить значок состояния. Каждое имя состояния должно быть уникально в своем классе. Состояния, ассоциированные со всей системой, глобальны, то есть видимы отовсюду, а область видимости вложенных состояний (дополнительное понятие) - ограничена соответствующей подсистемой. Все одноименные значки состояний на одной диаграмме обозначают одно и то же состояние.
На значках некоторых состояний полезно указать ассоциированные с ними действия. Как показано на рис. 5-18, действия обозначаются так же, как атрибуты и операции в значке класса. Мы можем увеличить значок, чтобы увидеть весь список действий, или, если нет необходимости указывать действия, можно удалить разделяющую линию и оставить только имя [Для совместимости с обозначениями Харела разделяющую линию можно вообще убрать]. Ассоциацию действий с состояниями мы обсудим позднее.
Рис. 5-19. Значок перехода из состояния в состояние.
Переходы. Событием мы называем любое происшествие, которое может быть причиной изменения состояния системы. Изменение состояний называется переходом. На диаграмме переходов и состояний он изображается значком, показанным на рис. 5-19. Каждый переход соединяет два состояния. Состояние может иметь переход само в себя; обычно есть несколько различных переходов в одно и тоже состояние, но все переходы должны быть уникальны в том смысле, что ни при каких обстоятельствах не может произойти одновременно два перехода из одного состояния.
Например, в поведении гидропонной теплицы играют роль следующие события:
• Посажена новая партия семян.
• Урожай созрел и готов к сбору.
• Из-за плохой погоды упала температура в теплице.
• Отказало охлаждающее устройство.
• Наступил заданный момент времени.
Как будет рассказано в следующей главе, идентификация событий, подобных этим, позволяет определить границы поведения системы и распределить обязанности по осуществлению этого поведения между отдельными классами.
Каждое из первых четырех перечисленных выше событий, вероятно, вызывает некоторое действие - например, начало или остановку выполнения некоторого плана сельскохозяйственных работ по посеву, включение нагревателя или посылку сигнала тревоги технику, обслуживающему систему. Отсчет времени - это другое дело: хотя секунды и минуты не имеют значения (посевы растут, очевидно, не так быстро), наступление нового часа или суток может вызвать некоторый сигнал, например, включить/выключить лампочки и изменить температуру в теплице, чтобы имитировать смену дня и ночи, необходимую для роста растений.
Действием мы называем операцию, которая, с практической точки зрения, требует нулевого времени на выполнение. Например, включение сигнала тревоги - действие. Обычно действие означает вызов метода, порождение другого события, запуск или остановку процесса. Деятельностью мы называем операцию, требующую некоторого времени на свое выполнение. Например, нагрев воздуха в теплице - деятельность, запускаемая включением нагревателя, который может оставаться включенным неопределенное время, до тех пор, пока не будет выключен явной командой.
Модель событий, передающих сообщения, которую предложил Харел, концептуально безупречна, но ее нужно приспособить к объектному подходу. При анализе мы можем давать предварительные названия событиям и действиям, в общих чертах отражая наше понимание предметной области. Однако, отображая эти понятия на классы, мы должны предложить конкретную стратегию реализации.
Событие может быть представлено символическим именем (или именованным объектом), классом или именем некоторой операции. Например, событие CoolerFailure (неисправность охлаждающего устройства) может обозначать либо литерал, либо имя объекта. Мы можем придерживаться той стратегии, что все события являются символическими именами и каждый класс с поведением, управляемым событиями, имеет операцию, которая распознает эти имена и выполняет соответствующие действия. Такая стратегия часто используется в архитектурах типа модель-представление-котроллер (model-view-controller), которая пришла из языка Smalltalk. Для большей общности можно считать события объектами и определить иерархию классов, которые представляют собой абстракции этих событий. Например, можно определить общий класс событий DeviceFailure (неисправность устройства) и его специализированные подклассы, такие как CoolerFailure (неисправность охлаждающего устройства) и HeaterFailure (неисправность нагревателя). Теперь извещение о событии можно связать с экземпляром класса-листа (например, CoolerFailure) или более общего суперкласса (DeviceFailure). И если выполнение некоторого действия назначено только при возникновении события класса CoolerFailure, то это означает, что все другие случаи отказа устройств должны намеренно игнорироваться. С другой стороны, если выполнение действия связано с событием DeviceFailure, то действие должно выполняться независимо от того, на каком устройстве произошел сбой. Продолжая в том же духе, мы можем сделать так, чтобы переходы из состояния в состояние были полиморфны относительно классов событий. Наконец, можно определить событие просто как операцию, такую как GardeningPlan::execute(). Это похоже на подход, который трактует события как имена, но в отличие от него здесь не требуется явного диспетчера событий.
Для нашего метода несущественно, какая из этих стратегий выбрана для разработки, если она последовательно проводится во всей системе. Обычно в замечаниях указывается, какая стратегия использована для данного конкретного автомата.
Действие можно записывать, используя синтаксис, показанный в следующих примерах:
• heater.startUp() - действие
• DeviceFailure - произошло событие
• start Heating - начать некоторую деятельность
• stop Heating - прекратить деятельность.
Имена операций или событий должны быть уникальны в области видимости диаграммы; там, где необходимо, они могут быть квалифицированы соответствующими именами классов или объектов. В случае начала или прекращения некоторой деятельности, она может быть представлена операцией (такой, как Actuator::shutDown()) или символическим именем (для событий). Когда деятельность соответствует некоторой функции системы, такой, как harvest crop (сбор урожая), мы обычно пользуемся символическими именами.
На каждой диаграмме состояний и переходов должно присутствовать ровно одно стартовое состояние; оно обозначается немаркированным переходом в него из специального значка, изображаемого в виде закрашенного кружка. Иногда бывает нужно указать также конечное состояние (обычно автомат, ассоциированный с классом или системой в целом, никогда не достигает конечного состояния; этот автомат просто перестает существовать после того, как содержащий его объект уничтожается). Мы обозначаем конечное состояние, рисуя немаркированный переход от него к специальному значку, изображаемому как кружок с закрашенной серединой.
Рис. 5-20. Диаграмма состояний и переходов для контроллера тепличной среды (EnvironmentalController).
Пример. До сих пор вводились значки, описывающие существенные элементы диаграмм состояний и переходов. В совокупности они предоставляют разработчику систему обозначений, достаточную для моделирования простого конечного плоского автомата, пригодного для описания приложений с ограниченным числом состояний. Системы, имеющие много состояний или обладающие сильно запутанным событийным поведением, которое описывается переходами по условию или в результате предыдущих состояний, требуют для построения диаграмм переходов более сложных понятий.
На рис. 5-20 показан пример использования существенных обозначений. Пример опять описывает гидропонную систему. Мы видим диаграмму состояний и переходов для класса EnvironmentalController, впервые введенного на рис. 5-5.
На этой диаграмме все события представляются символическими именами. Мы видим, что все объекты этого класса начинают свою жизнь в начальном состоянии Idle (ожидание); затем они изменяют свое состояние по событию Define climate, для которого не предполагается явных действий (считается, что это событие, то есть ввод климатического задания, происходит только в дневное время). Дальше динамическое поведение этого класса состоит в переключении между со-стояниями Daytime и Nighttime (день и ночь); оно определяется событиями Sunrise и Sunset (восход и закат) соответственно; с этими событиями связаны действия по изменению освещения. В обоих состояниях событие понижения или повышения температуры в теплице вызывает обратную реакцию (операция adjustTemperature(), которая является локальной в этом классе). Мы возвращаемся в состояние Idle, когда поступит событие Terminate climate, то есть будет отменено климатическое задание.
Дополнительные понятия
Элементы диаграмм состояний и переходов, которые мы только что описали, недостаточны для многих случаев сложных систем. По этой причине мы расширим наши обозначения, включив семантику карт состояний, предложенную Харелом.
Рис. 5-21. Действия, условные переходы и вложенные состояния.
Действия, ассоциированные с состояниями и условные переходы. Как показано на рис. 5-18, с состояниями могут быть ассоциированы действия. В частности, можно назначить выполнение некоторого действия на входе или выходе из состояния, при этом используется синтаксис следующих примеров:
• entry start Alarm - запуск процедуры при входе в состояние
• exit shutDown() - вызов операции при выходе из состояния.
Как и для переходов, можно назначить любое действие после ключевых слов entry и exit(вход и выход).
Деятельность можно ассоциировать с состоянием, используя синтаксис следующего примера:
• do Cooling - в данном состоянии заниматься этой деятельностью.
Этот синтаксис служит сокращенной записью явных указаний: "Начать деятельность при входе в состояние и окончить при выходе из него".
На рис. 5-21 мы видим пример использования этих обозначений. При входе в состояние Heating (нагревание) вызывается операция Heater::startUp(), а при выходе - операция Heater::shutDown(), то есть происходит запуск и остановка нагревания. При входе и выходе из состояния Failure (сбой), соответственно вызывается и прекращается сигнал тревоги (Alarm).
Рассмотрим также переход из состояния Idle в состояние Heating. Он совершается, если температура понизилась, но только в случае, если прошло больше пяти минут после того, как последний раз был выключен нагреватель. Это пример условного (или защищенного) перехода; условие представляется логическим выражением в скобках.
Вообще, каждый переход может быть ассоциирован либо с событием, либо с событием и условием. Допускаются и "переходы без события". В этом случае переход совершается сразу после завершения действия, связанного с состоянием, причем выполняется и действие, связанное с выходом из этого состояния. Если переход условный, он состоится только в случае, если условие выполнено.
Имеет значение порядок выполнения условного перехода. Пусть имеется состояние S, из которого при событии E совершается переход T с условием C и действием A. Переход T осуществляется в такой последовательности:
• Происходит событие E.
• Проверяется условие C.
• Если C удовлетворено, то выполняется переход T и действие A.
Это означает, что если условие C не выполнено, то переход не может быть осуществлен до тех пор, пока событие E не произойдет еще раз и условие C не будет проверено еще раз. Побочные эффекты при вычислении условия или выполнении действия, назначенного на выход, не могут отменить переход. Например, предположим, что произошло событие E, условие C выполнилось, но действие A, выполняемое при выходе из состояния S, изменило ситуацию так, что условие C перестало выполняться: переход T все равно состоялся.
Мы можем использовать еще и следующий синтаксис:
• in Cooling - выражение для текущего состояния.
Здесь используется имя состояния (которое может быть квалифицированным). Выражение истинно тогда и только тогда, когда система находится в указанном состоянии. Такие условия особенно полезны, когда некоторому внешнему состоянию нужно запустить переход по условию, связанному с некоторым вложенным состоянием.
Можно использовать в условии и выражение, налагающее ограничения по времени:
• timeout (Heating, 30) - выражение ограничения по времени.
Это условие выполняется, если система более 30 секунд находилась в состоянии Heating и остается в нем в момент проверки. Этот тип условия употребляется в системах реального времени для "переходов без события", так как защищает систему от зависания на долгое время в одном состоянии. Это выражение можно использовать для указания нижней границы времени нахождения в данном состоянии. Если приложить временное ограничение к каждому переходу с событием, выводящим из данного состояния, это будет равнозначно требованию, что система находится в каждом состоянии как минимум время, указанное в ограничении [Харел предложил "обобщенную завитушку" для обозначения двухсторонних границ по времени, но мы не будем обсуждать здесь его обобщения, так как условия исчерпания времени достаточно выразительны].
Что случится, если некое событие произойдет, а перейти в другое состояние нельзя либо потому, что не существует перехода для данного события, либо не выполняется условие перехода? По умолчанию это надо считать ошибкой: игнорирование событий обычно является признаком неполного анализа задачи. Вообще, для каждого состояния нужно документировать события, которые оно намеренно игнорирует.
Вложенные состояния. Возможность вложения состояний друг в друга придает глубину диаграммам переходов; эта ключевая особенность карт состояний Харела предотвращает комбинаторный взрыв в структуре состояний и переходов, который часто случается в сложных системах.
На рис. 5-21 показаны внутренние детали состояния Cooling, то есть вложенные в него состояния; для простоты мы опустили все его действия, включая действия при входе и выходе.
Объемлющие состояния, такие, как Cooling, называются суперсостояниями, а вложенные, такие, как Running, - подсостояниями. Вложенность может достигать любой глубины, то есть подсостояние может быть суперсостоянием для вложенных состояний более низкого уровня. Данное суперсостояние Cooling содержит три подсостояния. Семантика вложенности подразумевает отношение xor (исключающее или) для вложенных состояний: если система находится в состоянии Cooling (охлаждение), то она находится ровно в одном из подсостояний Startup (начальное), Ready (готовность) или Running (выполнение).
Чтобы проще ориентироваться в диаграмме переходов с вложенными состоя-ниями мы можем увеличить или уменьшить ее масштаб относительно выбранного состояния. При уменьшении вложенные состояния исчезают, а при увеличении проявляются. Переходы в скрытые на диаграмме подсостояния и выходы из них показываются стрелкой с черточкой, как переход в состояние Ready на рисунке [Если быть точными, то переходы Too hot и Ok относительно состояния Cooling также должны быть показаны на рис. 5-21 с черточкой, так как это переходы между подсостояниями].
Переходам между состояниями разрешено начинаться и кончаться на любом уровне. Рассмотрим различные формы переходов:
• Переход между одноуровневыми состояниями (такой, как из Failure в Idle или из Ready в Running) - простейшая форма перехода; его семантика описана в предыдущем разделе.
• Можно совершить переход непосредственно в подсостояние (как из Idle в Startup), или непосредственно из подсостояния (как из Running в Idle), или одновременно и то, и другое.
• Указание перехода из суперсостояния (как из Cooling в Failure через событие Failure) означает, что он осуществляется из каждого подсостояния этого суперсостояния. Такой переход пронизывает все уровни до переопределения. Это упрощает диаграмму за счет удаления банальных переходов, общих для всех подсостояний.
• Указание перехода в состояние с вложенными подсостояниями (например, предыдущий переход в состояние Failure) подразумевает переход к его начальному подсостоянию (по умолчанию).
История. Иногда, возвращаясь к суперсостоянию, мы хотели бы попасть в то его подсостояние, где мы были последний раз. Эту семантику мы будем изображать значком истории (буква H (History) внутри кружка, размещенного где-нибудь внутри значка суперсостояния). Например, на рис. 5-22 мы видим развернутое изображение состояния Failure. В самый первый раз, когда наша система переходит в него, она принимает начальное состояние по умолчанию Create log (создать журнал); что обозначено непомеченным переходом из закрашенного кружка внутри объемлющего состояния; когда журнал (log) создан, система переходит в состояние Log ready. После того, как сообщение о сбое занесено в журнал, мы возвращаемся обратно. Когда мы попадем в состояние Failure в следующий раз, нам не нужно будет опять создавать журнал, и мы перейдем прямо к Log ready, так как когда мы в последний раз выходили из состояния Failure, система находилась именно в этом подсостоянии.
Рис. 5-22. История событий.
Действие "истории" распространяется только на тот уровень, на котором она указана. Если мы хотим распространить ее действие на все нижние подуровни, то мы обозначим это, пририсовав к ее значку звездочку. Можно получить промежуточный результат, пририсовав значок истории только к отдельным подсостояниям.
Спецификации
Каждый элемент диаграммы переходов может иметь спецификацию, которая дает его полное определение. В отличие от спецификаций классов, спецификации переходов и состояний ничего не добавляют к уже описанному в этом разделе, поэтому нет необходимости обсуждать их специально.