ДЕКЛАРАТИВНЫЙ ПОДХОД К ВЛОЖЕНИЮ И НАСЛЕДОВАНИЮ АВТОМАТНЫХ КЛАССОВ
A.A. Астафуров
Научный руководитель - кандидат технических наук Д.Г. Шопырин
В работе предлагается декларативный подход к реализации автоматных объектов в рамках современных объектно-ориентированных императивных языков программирования со статической проверкой типов. Отличительной особенностью предлагаемого подхода является возможность использования наследования и вложения макросостояний.
Введение
Всякий раз, когда возникает необходимость реализации автоматов в объектно-ориентированных языках, мы сталкиваемся с различными подходами к решению этой задачи. Подходы к реализации автоматных систем можно разделить на императивные и декларативные.
Наиболее распространенным императивным подходом является паттерн проектирования State Design Pattern [1, 2]. Основными плюсами данного подхода являются распределение специфического поведения между состояниями, а также выполнение переходов в явном виде, непосредственно в коде. Главным недостатком является необходимость создания сложной иерархии при наличии большого числа состояний у автомата, которая может быть устранена при использовании шаблона Decorator Design Pattern [2, 3]. Кроме того, при императивном подходе в коде автоматного объекта присутствуют такие лишние детали, как явное делегирование вызова из контекста конкретному состоянию или вызов вложенного автомата.
Программа является декларативной, если она описывает то, чем она должна быть в конечном итоге, а не то, как именно этого достигнуть. Например, веб-страницы декларативны, так как они описывают то, как страница должна выглядеть (шрифт, размеры, текст, картинки), а не то, как на самом деле она должна быть отображена на экране компьютера. Этот подход сильно отличается от традиционного императивного подхода таких языков, как Fortran, C, Java, которые требуют от программиста конкретного алгоритма для выполнения. Иными словами, императивные программы делают алгоритм явным, оставляя цель неявной. Декларативные программы же делают цель явной, оставляя алгоритм неявным [4]. Существуют подходы, позволяющие реализовывать автоматы в полностью декларативном виде - например, подходы на основе XML. Плюсом таких подходов является полная изоморфность модели, а минусом - то, что они плохо применимы в реальных условиях.
В данной работе предлагается подход, являющийся компромиссом между декларативным и императивным подходами. От декларативного подхода берется возможность декларативного описания структуры автомата, от императивного подхода - описание логики переходов, так как она является частью автоматного кода и является императивной по своей природе.
При помощи декларативного метаязыка можно сконструировать автомат, описав его состояния. При этом достаточно легко удается решать задачу вложения автоматов, избавляя программиста от необходимости в явном виде указывать вызовы вложенных автоматов - все вызовы будут сделаны при помощи специальной библиотеки, контролирующей автомат и осуществляющей перехват вызовов. При помощи декларативного подхода удается также решить задачу наследования, позволяя наследовать автоматы, перекрывая их состояния, которые, в свою очередь, также могут наследоваться. Сама же возможность наследования позволяет по-новому взглянуть на автомат, рассматривая его как самостоятельный объект и применяя к нему принципы объектно-ориентированного дизайна. Все эти качества, совершенно необходимые для более эффективной реа-
лизации автоматов, к сожалению, отсутствуют в известных в настоящее время шаблонах и библиотеках.
Данная статья описывает способ применения декларативного автоматного программирования в современных объектно-ориентированных императивных языках со статической проверкой типов, иллюстрируя описанные подходы примерами программ на языке C#.
Описание подхода
В подходе используется модифицированная модель Statecharts. Основная разница между Statecharts [5] и SWITCH-технологией [6] состоит в том, что в SWITCH-технологии явно вводится понятие автомата. Семантика используемых в SWITCH-технологии графов переходов близка к семантике языка Statecharts, но не эквивалентна ей. В SWITCH-технологии вводится понятия автомата и вложения автоматов, но отсутствуют понятия вложенных и ортогональных состояний. Вложение и ортогонализация состояний в SWITCH-технологии реализуется посредством вложения автоматов и введения понятия «система автоматов» [7]. SWTICH-технология удобнее с точки зрения документации: при использовании вложения автоматов не возникает проблемы с нотацией, как это имеет место при использовании вложенных ортогональных состояний. Так, например, в языке Statecharts возникают трудности с расположением названия макросостояния, содержащего вложенные ортогональные состояния [5].
В данном подходе автомат и вложенные в него состояния рассматриваются как набор макросостояний. Действительно, ведь у состояния нет никакой специфики по отношению к автомату. Макросостояние может включать в себя другие макросостояния с их состояниями. С точки зрения программной реализации библиотеки автоматного программирования между состоянием и автоматом также нет никакой разницы. В рамках библиотеки, разрабатываемой в данном подходе, в системе будет присутствовать класс состояния, который, помимо стандартных свойств, таких как текущее состояние, вложенные макросостояния и т.д., реализует интерфейс, описывающий допустимые входные воздействия. Стоит также отметить, что в диаграмме UML 2 Sate Machine (State Chart в UML 1) [8] понятие автомата на диаграмме так же отсутствует: существуют только состояния, которые могут включать другие состояния, таким образом, решая вопрос вложения автоматов.
Благодаря отождествлению понятий состояния и автомата, а также введению термина «макросостояние», определение вложения следует читать так: «при получении сообщения состоянием это сообщение сначала передается всем его вложенным состояниям, а затем выполняется действие, связанное с этим сообщением». В настоящей работе мы будем считать, что входное воздействие сначала передается всем вложенным состояниям, а затем выполняется действие внутри состояния.
В рамках работы также рассматривается проблема наследования автоматов. Разрабатываемый подход должен позволять наследовать автоматы, переопределяя состояния и правила перехода между ними. Наследование автоматных объектов основано на перегрузке состояний базового автоматного объекта. Производный объект должен перегрузить поведение базового объекта как минимум в одном из его состояний. В производный объект могут быть добавлены новые состояния и переходы между ними [7].
Декларативный подход в рамках императивного языка
Несмотря на императивность многих современных языков программирования, в них тоже можно применять декларативный подход. Для этого необходимо инкапсулировать все недекларативные детали внутри библиотеки или модели, создавая ощущение
декларативности. В зависимости от языка и платформы это может быть достигнуто разными способами. Например, в языке Java и платформе Microsoft .NET существует механизм отражения (англ. reflection), позволяющий получать доступ к компонентам класса во время исполнения. Это позволяет давать декларативное описание поведения классов, а затем, во время исполнения, добавлять императивную составляющую.
Для более детального рассмотрения реализации декларативного подхода в императивном языке в качестве примера возьмем язык C# и платформу Microsoft .NET. Наличие такой конструкции, как атрибут, позволяет естественно применить декларативный подход в этом языке. Атрибуты в C# - это конструкции, позволяющие добавлять метаинформацию как к членам класса, так и к самим классам. В дальнейшем информация об атрибутах может быть получена во время исполнения библиотекой, инкапсулирующей недекларативные составляющие программы при помощи отражения.
Рассмотрим механизм выполнения декларативной программы в императивной среде. В рамках платформы .NET возможны следующие основные варианты:
• использование Context Bound Objects - применение встроенного в CLR механизма по перехвату сообщений, поступающих объекту;
• инструментализация сборок - модификация скомпилированного байт-кода с добавлением императивной составляющей, необходимой для выполнения программы в среде Microsoft CLR.
Context Bound Object (привязанный к контексту объект, CBO) - специальный объект, позволяющий контролировать все вызовы, поступающие к нему из контекста. В терминах Microsoft CLR контекст - это окружение, устанавливающее правила, которым будут следовать объекты, в нем находящиеся [9]. Таким образом, если объект привязан к контексту, он также привязан и к его правилам. Контекст создается при активации объекта. Общение объекта с внешним миром происходит при помощи сообщений. Среда Microsoft CLR предоставляет механизмы, позволяющие встраиваться в цепочку обработки сообщений. Это необходимо для их перехвата, а также для генерации или модификации сообщений, проходящих через границу контекста.
Таким образом, при использовании Context Bound Object мы можем контролировать вызовы, а значит, при использовании метаинформации, полученной из атрибутов, мы можем «добавлять» императивную часть в программу во время выполнения. Вся эта работа происходит незаметно для прикладного программиста и реализуется стандартными средствами CLR.
Отметим, что использование CBO оправдано в тех случаях, когда производительность не критична. CBO также выгодно использовать при создании прототипа будущей декларативной системы.
Другим подходом к добавлению императивной составляющей в декларативный код является инструментализация сборок. Инструментализация сборок подразумевает модификацию байт-кода сборки, полученной в результате компиляции. Как и везде, в качестве управляющей информации используются метаданные, добавленные при помощи атрибутов. Однако в этом случае вся работа делается на этапе разработки, а не во время выполнения. Пользуясь метаданными из атрибутов, служебная подпрограмма модифицирует байт-код при помощи Reflection.Emit (для .NET), добавляя в методы вызовы других методов, обеспечивающих конкретную реализацию декларативным директивам.
Данный подход хорош для продуктов, критичных к скорости, так как все дополнительные вызовы делаются напрямую, без дополнительных затрат на управление очередью сообщений, как это происходит в CBO. Явным минусом такого подхода являются трудности при отладке приложения. Из-за того, что байт-код сборки модифицируется, код, полученный в результате этой модификации, начинает расходиться с отладочной информацией, сгенерированной на этапе компиляции.
Реализация на платформе .NET
Для реализации декларативного подхода в императивных языках C# и Visual Basic.NET была разработана библиотека классов DOME (Declarative Object Machines Extension). На рис. 1 приведена диаграмма классов библиотеки DOME.
StateAttribute
+Type +Name
+StateAttrinbute(in Type) +StateAttribute(in Type, in Overrides)
, Г
InitialStateAttribute
Рис. 1. Основные классы библиотеки
• State - базовый класс для состояний.
o Container - экземпляр объемлющего класса State. Данное поле необходимо для доступа к объемлющему состоянию при вложении состояний. o CurrentState - текущее вложенное состояние. Используется при вложении состояний, а также непосредственно в автомате. o SetState(Type state) - меняет текущее состояние. В качестве параметра нужно указать тип (класс) состояния, в которое нужно перейти.
• StateAttribute - атрибут, применяемый к классам State и Machine, для описания возможных состояний автомата или объемлющего состояния.
o Type - .NET CLR - тип (класс) состояния
o Name - имя состояния. На данный момент система его автоматически устанавливает в строковое представление значения Type для удобства использования атрибута. Считается, что присутствие двух экземпляров одного состояния является бессмысленным, так что конфликта имен состояний при именовании при помощи строкового представления Type возникнуть не может. o Конструктор - Первый конструктор принимает тип (класс) состояния. Перегруженный конструктор имеет два параметра и предоставляет механизм перекрытия состояний. Для этого необходимо в качестве Type указать тип (класс) нового состояния, а в качестве Overrides указать тип (класс) состояния, которое будет перекрыто. В ходе создания экземпляра атрибута, в случае перекрытия, будет также произведена проверка того, что перекрываемое состояние присутствует выше в иерархии.
• InitialStateAttribute - имеетто же значение, что и StateAttribute, и применяется для обозначения стартового состояния автомата или стартового вложенного состояния.
В качестве первого примера рассмотрим простейший автомат радара, состоящий из двух состояний: ON и OFF (рис. 2).
Для начала определим 3 интерфейса. Интерфейс IOn для состояния ON будет содержать только один метод E1 - т.е. это единственное событие, которое мы обрабатываем, находясь в этом состоянии. По такому же принципу создадим интерфейс IOff. Интерфейс IRadar будет описывать весь автомат и наследовать интерфейсы IOn и IOff:
State
+Container : State -CurrentState : State +SetState(in state : State)
public interface IOn {
void E0(); }
public interface IOff {
void E1(); }
public interface IRadar : IOn, IOff { }
Автомат радара A0
1. ON ^
V J
i e0 el I
r 0. OFF Л
t J
Рис. 2. Автомат радара, состоящий из двух состояний
Далее необходимо реализовать классы состояний ON и OFF:
class On : State, IOn
{
public void E1() {
Container.SetState(typeof(Off)); }
}
class Off : State, IOff {
public void E1() {
Container.SetState(typeof(On)); }
}
Поле Container является ссылкой на автомат, которому принадлежит данное состояние. Метод SetState(Type) меняет текущее состояние автомата.
Наконец, реализуем сам автомат радара, наследуясь от класса State и реализовав интерфейс IRadar:
[State(typeof(On)), State(typeof(Off))]
class Radar : State, IRadar {
public void E0() { }
public void E1() { } }
Особое внимание стоит обратить на атрибуты класса Radar. Именно они указывают на то, что автомат Radar будет содержать в себе 2 состояния, On и Off, реализованные в соответствующих классах.
Во время выполнения программы все вызовы к Radar будут перехватываться системой и передаваться методу текущего состояния с идентичной сигнатурой, а затем будут выполнен метод самого автомата. В случае отсутствия у текущего состояния метода с идентичной сигнатурой, вызов перейдет непосредственно к методу автомата.
Реализация вложения
Для описания вложения рассмотрим более сложный пример - автомат героя-бойца из компьютерной игры. Боец реагирует на два события - таймер для обновления состояния и нажатие на кнопку «вверх» для прыжка. В прыжке боец меняет состояние: отталкивается, летит, падает, встает на землю.
Tick
Рис. 3. Автомат бойца сдетализацией состояния прыжка
Как видно на рис. 3, состояние Jumping имеет вложенные состояния: Rising, Falling, Hovering и Finished. Рассмотрим вложение в исходном коде:
[InitialState(typeof(Jumping.Rising) ) , State(typeof(Jumping.Falling)), State(typeof(Jumping.Hovering) ) , State(typeof(Jumping.Finished))]
public class Jumping : State, IFighter {
public void Tick() {
if (CurrentState is Finished)
Container.SetState(typeof(Fighter.Main)); }
public void ButtonPressed(Keys key)
{}
public bool InAir() {
return true; }
public class Rising : State, ITickable {
public void Tick() {
Console.WriteLine("rising");
Container.SetState(typeof(Hovering)); }
}
public class Hovering : State, ITickable {
public virtual void Tick() {
Console.WriteLine("hovering");
Container.SetState(typeof(Falling)); }
}
public class Falling : State, ITickable {
public void Tick() {
Console.WriteLine("falling");
Container.SetState(typeof(Finished)); }
}
public class Finished : State, ITickable {
public void Tick() {
// }
} }
В этом случае событие будет сначала передано текущему вложенному состоянию, а затем обработано соответствующим методом состояния Jumping. Как и в предыдущем примере, вызов передается при помощи перехвата сообщения к Context Bound Object.
Реализация наследования
При помощи декларативного подхода удается очень легко и интуитивно понятно описывать наследование автоматов. Для этого достаточно унаследовать класс от базового автомата. Для перекрытия тех или иных состояний необходимо описать это в метаданных. Разумеется, базовые состояния можно наследовать так же, как и автоматы, перекрывая их вложенные состояния или добавляя новые.
В качестве примера поставим задачу перекрытия одного из состояний в автомате Fighter из предыдущего примера. Для усложнения задачи предположим, что необходимо перекрыть состояние Hovering, вложенное в состояние Jumping. Для этого
необходимо создать наследника класса Fighter, назовем его EasternFighter, и атрибутами описать, что мы перекрываем состояние Fighter.Jumping:
[State(typeof(EasternJumping), typeof(Fighter.Jumping))]
public class EasternFighter : Fighter {
}
Действительно, перекрывая вложенное состояние Hovering, мы, тем не менее, перекрываем и само состояние Jumping. Теперь необходимо создать наследника класса-состояния Jumping, чтобы, собственно, перекрыть его вложенное состояние Hovering новым состоянием EasternHovering:
[State(typeof(EasternHovering), typeof(Fighter.Jumping.Hovering))]
public class EasternJumping : Fighter.Jumping {
}
Наконец, необходимо реализовать само состояние EasternHovering, создав наследника от Jumping.Hovering.
public class EasternHovering : Fighter.Jumping.Hovering
{
public override void Tick() {
Console.WriteLine("Eastern Hovering");
Container.SetState(typeof(Fighter.Jumping.Falling)); }
}
Таким образом, когда новый автомат будет вызван, при переходе в состояние Jumping будет использован новый класс EasternJumping, который, в свою очередь, использует перекрытое состояние EasternHovering, о чем сообщает при протоколировании на консоль.
Заключение
В рамках работы был предложен декларативный подход к реализации автоматных объектов. В работе описаны способы реализации декларативного подхода в императивных языках, а также предложена библиотека, позволяющая реализовать данный подход на платформе Microsoft .NET. Использование библиотеки проиллюстрировано примерами реализации наследования и вложения с ее использованием.
Данный подход к реализации автоматов помогает решить проблему совместного использования декларативных и императивных языков, позволяя применять декларативный объектно-ориентированный подход к дизайну автоматов в императивных языках. Благодаря тому, что данная концепция описана на уровне парадигмы декларативного программирования без привязки к конкретному языку программирования, изложенные подходы можно применять и в других языках, позволяющих реализовать декларативный подход, например, Java.
В рамках будущей деятельности планируется также более детально рассмотреть вопрос создания библиотеки, подобной DOME в языке Java, а также подробнее изучить вопрос виртуальных и невиртуальных состояний и их перекрытия.
Литература
1. Гамма Э., Хелм Р., Джонсон Р., Влиссндес Дж. Приемы объектно-ориентированного проектирования. Паттерны проектирования. СПб: Питер, 2001.
2. Adamczyk P. The Anthology of the Finite State Machine Design Patterns // The 10th Conference on Pattern Languages of Programs, 2003.
3. Odrowski J. and Sogaard P. Pattern Integration - Variations of State // Proceedings of PLoP96.
4. Czarnecky K. and Eisenecker U.W. Generative Programming Methods, Tools and Applications. Addison-Wesley, 2000.
5. Harel D. Statecharts: A visual formalism for complex systems // Sci.Comput. Program. 1987. Vol. 8. P. 231-274.
6. Шалыто A.A. SWITCH-технология. Алгоритмизация и программирование задач ло-гическогоуправления. СПб: Наука, 1998.
7. Шопырин Д.Г. Методы объектно-ориентированного проектирования и реализации программного обеспечения реактивных систем./ СПб.: СПбГУ ИТМО, 2005.
8. Буч Г., Рамбо Дж., Джекобсон A. UML. Руководство пользователя./ М.: ДМК, 2000. 432 с.
9. Box D. and Sells C. Essential .NET Vol. 1, The Common Language Runtime. Addison-Wesley, 2002.