АВТОМАТНОЕ РАСШИРЕНИЕ ЯЗЫКА C#
М.Г. Раер
Научный руководитель - д.т.н., профессор А.А. Шалыто
Работа рассматривает паттерн проектирования State, его модификацию State Machine, описывает модификацию State Machine для языка C# на основе синтаксических событий языка.
Введение
Часто поведение объекта, его реакция на определенные события зависит от состояния, в котором он находится. Такие объекты принято описывать конечными автоматами. Существует большое количество способов реализации автоматов в программировании [1, 2]. Рассматривается весь спектр вариантов, от абсолютно не объектно-ориентированного, когда автомат реализуется вложенными операторами if или switch, до полностью объектно-ориентированного, где все элементы автомата (состояния, события, переходы, действия) реализованы как классы. Самой распространенной объектно-ориентированной реализацией является паттерн State [3]. В работе [1] рассмотрен список различных модификаций этого паттерна.
Паттерн проектирования State предлагает вынести код, отвечающий за поведение объекта в неком состоянии, в отдельный класс, и в методах этого класса, в зависимости от неких условий, производить переход в другие состояния. Таким образом, получается, что код, отвечающий за смену состояний, рассредоточен по классам состояний, что затрудняет его модификацию и поддержку и нарушает принцип централизованной логики управлений, предложенной в SWITCH-технологии [4]. Также возникает зависимость между классами состояний, так как они должны «знать» друг о друге.
Авторами работы [6] был предложен новый паттерн State Machine, устраняющий эту зависимость и централизующий логику управления в одном месте. В работе приводилась реализация на языке Java, поэтому в классы состояний пришлось ввести члены события (объекты специального класса Event) и наследовать состояния от базового класса StateBase. Это вносит некоторые ограничения на использование классов в качестве классов-состояний.
В работе [7] предлагается расширение языка Java для более удобной реализации паттерна State Machine.
В настоящей работе предлагается реализация паттерна State Machine для языка C# [8], снимающая указанные выше ограничения. Также предлагается дополнение языка C# (State#) для более эффективной реализации паттерна. В качестве состояний становится возможным использовать произвольный класс, автомат может реализовывать произвольное количество автоматных интерфейсов. В язык State# вводится возможность автоматического протоколирования, событие смены состояния. Условия перехода можно задавать не только событиями, но и дополнительными условиями, предусмотрена возможность описывать действия на переходе.
1. Обзор паттернов state и state machine 1.1. Паттерн State
Паттерн State позволяет объекту изменять свое поведение в зависимости от своего состояния. Он входит в группу паттернов поведения.
Вводится абстрактный класс State, определяющий интерфейс объекта. Подклассы класса State задают поведение объекта. Каждому состоянию соответствует подкласс. Определяется основной класс Context. В нем хранится объект текущего состояния. Класс Context делегирует все запросы из интерфейса State этому объекту.
Методы наследников класса State могут изменять значение этого объекта, изменяя тем самым состояние системы.
1.2. Паттерн State Machine
В работе [6] авторами предлагается паттерн State Machine, устраняющий вышеуказанные недостатки. На рис. 1 изображена его структура.
Т7^
I
I I
j
j
Рис. 1. Структура паттерна State Machine
Для того чтобы избавиться от зависимости классрВпрвЕтрИяЭиЙЕмежду собой, в паттерне State Machine в классы состояний были добавлены события.
Приводилась реализация на языке Java. I tVGnto IП К
События - это объекты, которые передаются состояниями контекступи сообщают о необходимости смены состояния. ^ЛербХодо&Цейтраяизу-
ется в классе контекста. Класс контекста на основе текущего состояния и пришедшего события определяет новое состояние.
Добавление классу состояния события реализовывалось путем добавления в этот класс члена некого класса Event.
public class ConnectedState <AI extends IConnection> extends StateBase<AI> implements IConnection {
public static final Event DISCONNECT = new Event("DISCONNECT"); public static final Event ERROR = new Event("ERROR");
}
Кроме того, любое состояние через базовый класC^tafleBafe должно было агрегировать интерфейс IEventSink, для уведомления контекста о смене состояний.
public abstract class StateBase<AI> { protected final IEventSink eventSink;
}
Это обязывает специально разрабатывать классы состояний с учетом этих требований и лишает пользователя возможности использовать в качестве классов состояний классы, изначально не предусмотренные для этого, например классы стандартных библиотек. Отсутствие в классе контекста членов событий делает невозможным использование его в качестве класса состояния, а значит и не возможным использование автомата в качестве состояния другого автомата. Это значит, что невозможно иметь один автомат вложенным в другой.
2. Реализация state# (S#)
В этой работе предлагается реализация State# - реализация паттерна State на языке C#. Она снимает ограничения на классы состояния. На рис. 2 изображена структура реализации State#.
Рис. 2. Структура реализации State#
2.1. События
В качестве средства устранения недостатков, описанных в предыдущей главе, был выбран язык С# [8]. Этот язык содержит понятие событие на синтаксическом уровне. События языка С# и предлагается использовать в качестве событий, которые порождаются состояниями для извещения контекста.
public class State1 {
public event EventHandler SomethingHappened;
}
public class Context { public Context() {
state1.SomethingHappened += new EventHan-
dler(state1_SomethingHappened); }
pulic void statel SomethingHappened(object sender, EventArgs e) { CurrentState = state2;
}
}
Таким образом, мы избавляемся от использования объектов класса Event, и переходим к использованию синтаксически определенных в языке C# событий. События объявлены во многих библиотечных классах. Они являются стандартизованным механизмом оповещения объектом о какой-либо ситуации. Это позволяет использовать библиотечные классы в качестве классов состояний.
Также события могут присутствовать и в классе контекста, а значит, возможно, использование класса контекста в качестве класса состояния для другого контекста. Это позволяет реализовать вложенные автоматы, когда какое-либо состояние объемлющего автомата состоит из нескольких подсостояний вложенного.
public class Contextl { private Statel statel; private State2 state2; private State3 state3;
}
public class Context2 { private State4 statel; private Contextl state2;
}
Здесь автомат Contextl состоит из трех состояний: Statel, State2 и State3. А автомат Context2 состоит из двух состояний: State4 и Contextl. Состояние Contextl, так как в свою очередь является автоматом, состоит из трех под-состояний: Statel, State2, State3.
2.2. Автоматный интерфейс
Автоматным интерфейсом назовем тот интерфейс, реализация методов которого зависит от состояния автомата. Его методы реализуются в классе автомата делегированием вызова соответствующего метода у текущего состояния (рис. 3).
Рис. 3. Делегирование операций текущему состоянию автомата
Все классы состояний должны реализовывать все автоматные интерфейсы. Однако это требование не распостраняется на неавтоматные интерфейсы. Класс состояния может и не реализовывать некий неавтоматный интерфейс, который реализуется автоматом.
В паттерне State Machine предполагалось, что автомат реализует один автоматный интерфейс. Это ограничение несколько искусственно. Предлагается использовать произвольное количество автоматных интерфейсов. Это может быть обосновано при использовании уже готовых, библиотечных интерфейсов. Единственное требование - каждый класс состояния автомата должен реализовывать все автоматные интерфейсы. При этом и класс контекста, и классы состояний могут реализовывать произвольное количество неавтоматных интерфейсов.
2.3. Действия на переходах
Switch-технология [4] предлагает задавать действия, которые выполняются при переходе автомата из одного состояния в другое. State# сохраняет эту возможность. Действия на переходе могут быть заданы в обработчике события перехода.
public class Context { public Context() {
statel.SomethingHappened += new EventHan-
dler(state1 SomethingHappened); }
public void statel SomethingHappened(object sender, EventArgs e) { // Действия на переходе CurrentState = state2;
}
}
2.4. Сравнение реализации State Machine на языках Java и С#
Отсутствие встроенных в язык Java событий вынуждает к определению класса Event и добавлению в классы состояний экземпляров этого класса. Также класс состояния вынужден имплементировать интерфейс IEventSink для нотификации контекста о смене состояния.
Встроенные в язык C# события избавляют от необходимости в событиях-объектах и реализации интерфейса.
В реализации на языке Java обработка событий происходила в одном методе castEvent, который был методом интерфейса IEventSink. В этом методе следующее состояние определяется либо с использованием хэш-таблиц, либо при помощи оператора switch. Первое решение не позволяет задавать действия на переходах и проверять дополнительные условия, а второе делает код метода громоздким.
Реализация на языке C# использует отдельный метод для каждого перехода. Этот метод - обработчик события состояния. В него можно добавить проверку дополнительных условий для перехода и действия на переходе, код, который должен выполняться при переходе.
3. Язык State# (S#)
Для эффективной реализации подхода, изложенного в главе 2, предлагается расширение языка C#. Новый расширенный язык будет надмножеством языка C#, из чего следует, что ему будут принадлежать все программы, написанные на языке C#.
Введение в язык дополнительных синтаксических конструкций упрощает реализацию на нем шаблонных решений, паттернов.
Язык С# был разработан компанией Microsoft как язык для их новой платформы Microsoft .Net Framework. C# - это объектно-ориентированный язык высокого уровня. Как и многие современные языки C# - си-подобный язык. Однако для большей гибкости C# содержит такие дополнительные элементы, как свойства, делегаты и события.
С помощью событий языка объект сообщает всем желающим о каком-нибудь событии, произошедшим с ним. Событие вызывает код подписчиков, которые тем или иным образом реагируют на него.
Именно события позволяют нам достичь поставленной задачи. Любой класс, имеющий события и реализующий некий интерфейс, можно трактовать как состояние. Тогда другой класс (контекст), подписываясь на эти события, сможет сосредоточить логику переходов между состояниями.
3.1. Класс автомата
Для описания класса автомата в язык вводится новое ключевое слово automaton (англ. автомат). Оно используется вместо ключевого слова class, которое применяется в языке C# и обозначает классы специального вида, классы автоматов.
public automaton MyAutomaton
Автомат может содержать все члены, которые может содержать класс языка C#:
■ поля;
■ методы;
■ свойства;
■ индексаторы;
■ перегрузки операторов;
■ события.
3.2. Наследование и реализация интерфейсов
Также как класс в языке C#, в языке State# автомат может наследовать один класс. В State# базовый класс может быть обычным классом или автоматом. Однако класс автомата должен реализовывать как минимум один автоматный интерфейс и произвольное количество обычных интерфейсов. Перед автоматными интерфейсами ставится ключевое слово auto.
public automaton MyAutomaton : auto Il, I2, auto I3 { }
В этом примере объявляется класс автомата MyAutomaton, который реализует автоматные интерфейсы I1, I3 и обычный интерфейс I2.
Для всех состояний, включенных в автомат, классы этих состояний должны реа-лизовывать все автоматные интерфейсы. При этом классы состояний могут реализовы-вать и другие интерфейсы дополнительно.
Для каждого метода из каждого автоматного интерфейса в классе автомата создается неявная реализация, которая вызывает соответствующий метод у текущего состояния. Поэтому явно реализовывать методы из автоматных интерфейсов в классе автомата не нужно.
3.3. Состояния
Состояниями в классе контекста будут считаться члены-поля, объявленные с использованием ключевого слова state в начале объявления. Класс членов-состояний может быть любым, в том числе классом другого контекста.
public automaton MyAutomaton : auto I1, I2, auto I3 { state MyStateClassl statel;
}
В работе [7] для обозначения стартового состояния оно помещалось первым в класс контекста. Однако такая семантика нетипична для таких объектно-ориентированных языков, как Java и C#. В них не важен порядок объявления членов класса. Чтобы избежать нетипичной семантики, предлагается для стартового состояния использовать дополнительное ключевое слово initial.
public automaton MyAutomaton : auto I1, I2, auto I3 { state MyStateClassl statel; initial state MyStateClass2 state2; state MyStateClass3 state3;
}
После объявления состояния в фигурных скобках помещается информация о переходах из этого состояния.
Каждый переход начинается с ключевого слова transition. Затем идет ключевое слово to, за которым указывается имя члена-состояния, в которое осуществляется переход. Затем - ключевое слово when, после которого - имя публичного события, которое провоцирует переход и присутствует в классе состояния.
Затем идет опциональное ключевое слово incaseof, после которого в круглых скобках можно указать булево выражение, являющееся условием перехода.
В конце можно указать действия, совершаемые на переходе. Для этого они указываются в фигурных скобках после ключевого слова actions.
public automaton MyAutomaton : auto Il, I2, auto I3 { state MyStateClassl statel
{
transition to state2 when Happenedl
transition to state3 when Happened2
};
initial state MyStateClass2 state2
{
transition to statel
when SomethingHappened incaseof (someBooleanExpression)
actions {
Console.Writeln("!");
}
};
state MyStateClass3 state3;
}
3.4. Автоматическое протоколирование
Одним из принципов SWITCH-технологии является протоколирование работы автомата. Протоколирование позволяет отловить большое количество ошибок. Поэтому в язык State# добавлена возможность автоматического протоколирования работы автомата.
Включение или выключение протоколирования не должно происходить в коде программы, а должно управляться извне.
Протоколирование включается в конфигурационном файле проекта. Он представляет собой xml-документ [12]. Протоколирование включается путем добавления в него секции automatonLog c атрибутом Enabled со значением true. Далее нужно включить протоколирование непосредственно для экземпляра автомата. Это делается добавлением в секцию automatonLog тэга automatonLogInstance со следующими параметрами:
• Name - имя протоколируемого автомата для ссылки на него;
• Enabled - true или false - включено или выключено протоколирование;
• File - путь к файлу, в который будет вестись протоколирование.
Чтобы связать экземпляр автомата с записью в конфигурационном файле, ему необходимо задать имя. Для этого используется конструктор с именем в качестве первого параметра. Такие конструкторы будут созданы автоматически для каждого конструктора, созданного пользователем. Если автомат будет создан без передачи имени в конструкторе, то имя будет назначено автоматически в формате: <имя_класса_автомата>_<номер экземпляра>
MyAutomaton myAutomaton = new MyAutomaton("MyAutomatonl");
В результате в файле, заданном в конфигурационном файле для автомата с именем MyAutomatonl, будет сохранена вся информация о переходах автомата из одного состояния в другое.
3.5. Извещение о смене состояния
Кроме автоматического протоколирования, пользователю предоставляется возможность гибко настраивать реакцию системы на смену состояния автомата. Для этого каждый класс контекста предоставляет событие StateChanged типа State-ChangedEventHandler, которое происходит каждый раз при смене состояния.
delegate void StateChangedEventHandler (object sender, StateChangedEven-tArgs e)
В параметрах события sender представляет собой объект контекста. Параметр e содержит два открытых свойства OldState и NewState типа string, возвращающих имена членов состояний в классе контекста.
3.6. Сравнение языков State Machine и State#
Язык State Machine [7] является развитием языка Java и транслируется в Java. Язык State# является развитием языка C# и транслируется в C#. Язык State# не определяет синтаксиса для определения классов состояний, так как для реализации паттерна State Machine на языке State# в качестве классов состояний используются обычные классы. Рассмотрим недостатки языка State Machine:
1. класс автомата должен реализовывать ровно один интерфейс, который и считается автоматным;
2. инициализация объектов состояний в конструкторе автомата описывается с помощью нового оператора @= вместо оператора = ;
3. стартовым считается состояние, описанное в автомате первым. Описание состояний в автомате похоже на описание членов класса. Наделять семантикой порядок определения членов несколько нетрадиционно.
Язык State# устраняет эти недостатки, а также предоставляет дополнительные возможности:
1. возможность задавать булевские условия перехода;
2. возможность задавать действия на переходах;
3. генерация события о смене состояния;
4. настраиваемое протоколирование.
Заключение
В настоящей работе рассмотрен паттерн проектирования State Machine, являющийся развитием паттерна State.
Предлагается реализация паттерна State Machine, основанная на использовании событий языка C# как средства взаимодействия классов состояний и класса контекста. Это позволяет использовать стандартные библиотечные классы в качестве классов состояний. Использование событий языка может существенно ускорить разработку, так как позволит использовать уже готовые решения.
В рамках настоящей работы предложено расширение языка C# (State#) для более удобной реализации этого паттерна. Предложен синтаксис описания автоматов и разработан транслятор c языка State# на язык С#.
Транслятор поддерживает использование протоколирования. Протоколирование может быть полезно как средство отлаживания и тестирования. Управление протоколированием производится без перекомпиляции проекта, в конфигурационном файле. Кроме того, в автомат транслятором добавляется событие о смене состояния, которое может быть использовано извне.
В качестве расширения данной работы может быть рассмотрено написание модуля для Microsoft Visual Studio для автоматической трансляции программ на языке State#, написание транслятора с языка C# 2.0 или C# 3.0 и трансляция непосредственно в Microsoft Intermediate Language.
Литература
1. Adamczyk P. The Antology of the Finite State Machine Design Patterns. // http://jerry.cs.uiuc.edu/~plop/plop2003/Paperes/Adamczyk-State-Machine.pdf
2. Adamczyk P. Selected Patterns for Implementing Finite State Machines. // http://pinky.cs.uiic.edu/~padamczy/fsm
3. Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Приемы объектно-ориентированного проектирования. Паттерны проектирования. СПб: Питер, 2001.
4. Шалыто А. А. Switch-технология. Алгоритмизация и программирование задач логического управления. СПб.: Наука, 1998.
5. Николаенко А. Static Finite State Machime. // RSDN Magazine. 2005. № 3.
6. Шамгунов Н.Н., Корнеев Г. А., Шалыто А. А. State Machine - новый паттерн объектно-ориентированного проектирования. // Информационно-управляющие системы. 2004. № 5.
7. Шамгунов Н.Н., Корнеев Г.А., Шалыто А.А. State Machine - расширение языка Java для эффективной реализации автоматов. // Информационно-управляющие системы. 2005. № 1.
8. Троелсен Э. C# и платформа .NET. СПб.: Питер, 2002.
9. Либерти Д. Программирование на C#. СПб-Москва: Символ, 2003.
10. Microsoft Developer Network // http://msdn.microsoft.com
11. ANTLR // http://www.antlr.org
12. XML Specification // http://www.w3.org/XML/
13. XML Schema // http://www.w3.org/XML/Schema/
14. Рихтер Д. Программирование на платформе Microsoft .Net Framework. СПб: Питер, 2005.