IronPython – конфигурационный язык
Пример использования IronPython для создания конечного продукта.
Требовалось создать конечный автомат для действующего продукта. Продукт был инструментом управления проектами и поэтому имел принцип задания, основанный на переходах действий пользователей из одного состояния в другое. Также требовалось, чтобы конечный автомат можно было настраивать по/для разных клиентов. По сути, конечный автомат является большой блок-схемой, но реализация должна была позволять полностью вскрыть блок-схему первых клиентов и заменить ее на абсолютно другую блок-схему для следующего клиента.
Первой мыслью было реализовать это путем моделирования конечного автомата в виде метаданных в базе данных. Однако анализ конечного автомата первых клиентов показал, что число пограничных случаев затруднит такую реализацию и потребует изменений кода после продажи продукта другим клиентам с учетом их собственных пограничных случаев. Часть конечного автомата была уже написана на C#, поэтому пришла мысль написать конечный автомат на C#, хранить код в базе данных и компилировать его при выполнении. Но лучше всего подошел IronPython. Интерпретатор IronPython встраивается в код .NET и позволяет писать разный код для разных клиентов. Книга IronPython в действии Майкла Фурда описывает язык IronPython и приемы его использования вместе с C#.
В созданном прототипе на C# было много вспомогательных методов для работы с бизнес-сущностью задания и даже для создания проекта на сервере проекта! Не хотелось отказываться от них, поэтому был создан абстрактный класс, и затем был реализован подкласс на IronPython. Абстрактный класс приведен ниже:
namespace Sharpcoder.BusinessLogic
{
public abstract class StateMachine
{
public StateMachine()
{
// Здесь создаются разные объекты доступа к данным, используемые вспомогательными //методами
}
/// <span class="code-SummaryComment"><summary>
</span> /// Проводит задание через процесс закрытия этапов.
/// <span class="code-SummaryComment"></summary>
</span> /// <span class="code-SummaryComment"><param name="job"></param>
</span> public abstract void Transition(Job job);
// Разные вспомогательные методы, в том числе CreateProject,
/создающий проект в сервере проекта
}
Подкласс реализован на IronPython, для этого пришлось импортировать используемые классы из кода на C# (например, Job) и затем создать производный класс от абстрактного класса StateMachine и реализовать абстрактный метод Transition(переход). Ссылка на self(сам) в списке параметров метода Transition означает, что это метод экземпляра.
from Sharpcoder.BusinessEntities import (
Job, JobTask
)
from Sharpcoder.BusinessLogic import StateMachine
class Customer1StateMachine(StateMachine):
# Проводит задание через процесс закрытия этапов.
# param job
def Transition(self, job):
return None
# Transition - End
Чтобы суметь использовать реализацию конечного автомата на IronPython, надо было считать код, скомпилировать его и затем хранить описатель конечного автомата, чтобы его можно было использовать когда-нибудь позже. При этом немного снижается производительность, поэтому было решено использовать локатор служб, реализованный в виде синглтона, создаваемого при запуске из-за того, что происходит при запуске. Интересные части этого класса – куски, создающие объект конечного автомата – показаны ниже. Поскольку надо было ссылаться на некоторые классы из основной части приложения (особенно подкласс StateMachine), пришлось загрузить сборки, где они определены. Так сделано с вызовами runtime.LoadAssembly() – этот код наверняка можно было бы реализовать лучше. Код также предполагает, что код на IronPython определяет класс, расширяющий абстрактный класс StateMachine, создает экземпляр этого нового класса и затем присваивает ссылку на него переменной по имени machine – позже посредством переменной machine можно получить описатель для заданного конечного автомата.
namespace Sharpcoder.BusinessLogic
{
public class StateMachineLocator
{
// Потокобезопасный, отложенный синглтон
/// <span class="code-SummaryComment"><summary>
</span> /// Создает конечный автомат.
/// <span class="code-SummaryComment"></summary>
</span> private void InitFactory()
{
ScriptEngine engine = Python.CreateEngine();
ScriptRuntime runtime = engine.Runtime;
ScriptScope scope = engine.CreateScope();
// Добавить нужные сборки – вероятно, это требует доработки
runtime.LoadAssembly(typeof(Job).Assembly); // Sharpcoder.BusinessEntities
runtime.LoadAssembly(GetType().Assembly); // Sharpcoder.BusinessLogic
ScriptSource script =
engine.CreateScriptSourceFromString
(GetStateMachineSource(), SourceCodeKind.Statements);
code.Execute(scope);
scope.TryGetVariable<StateMachine>("machine", out _machine);
}
public StateMachine StateMachine
{
get { return _machine; }
}
}
}
GetStateMachineSource() можно реализовать так, как вы сочтете нужным. Для прототипа код на IronPython помещен в файл, назначенный встроенным ресурсом. В итоге будут внесены изменения для чтения кода из базы данных, чтобы была возможность делать разные конечные автоматы для разных клиентов.
/// <span class="code-SummaryComment"><summary>
</span> /// Читает конечный автомат, написанный на IronPython.
/// <span class="code-SummaryComment"></summary>
</span> /// <span class="code-SummaryComment"><returns>The source code.</returns>
</span> private String GetStateMachineSource()
{
Assembly assembly = Assembly.GetExecutingAssembly();
using (Stream stream =
assembly.GetManifestResourceStream("Sharpcoder.Job.Customer1StateMachine.py"))
{
using (StreamReader reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
}
Сейчас имеется весь код, необходимый для создания конечного автомата и его использования в приложении. Осталось лишь дополнить конечный автомат, чтобы он делал нечто полезное! Часть кода конечного автомата приведена ниже (это незаконченная работа: обратите внимание на жестко заданные идентификаторы пользователей и т.д.):
class Customer1StateMachine(StateMachine):
# Различные константы
# Переводит задание в состояние 1.007
# param job
def _Event_1007(self, job):
self.UpdateJobStage(job, self.JOB_STAGE_1_ID)
self.UpdateJobState(job, self.JOB_STATE_1007_ID)
self.UpdateJobStatus(job, self.JOB_STATUS_OPEN_AWAIT_CUST_QUAL_ID)
# Пока жестко задан как Стеф, однажды придется
# искать, чтобы найти менеджера
# ???
self.CreateJobTask(job, self.JOB_TASK_TYPE_CUST_PRE_QUAL_ID, self.STEFF_USER_ID)
self.CreateProject(job)
# _Event_1007 - End
# Переводит задание в состояние 1.010
# param job
def _TransitionToState_1010(self, job):
self.UpdateJobState(job, self.JOB_STATE_1010_ID)
self.UpdateJobStatus(job, self.JOB_STATUS_OPEN_AWAIT_QUAL_ID)
# Пока жестко задан как Стеф, однажды придется искать, чтобы найти
# менеджера
self.CreateJobTask(job, self.JOB_TASK_TYPE_CUST_INFO_ID, self.STEFF_USER_ID)
# _TransitionToState_1010 - End
# Переводит задание в состояние 1.010
# param job
def _Event_1009(self, job):
self.TransitionToState_1010(job)
# _Event_1009 - End
# Переводит задание в состояние 1.010
# param job
def _Event_1010(self, job):
self.UpdateJobStage(job, self.JOB_STAGE_1_ID)
self._TransitionToState_1010(job)
#self.CreateProject(job)
# _Event_1009 - End
# Выполняет смену состояний для нового задания
# param job – новое задание.
def _NewJobStateTransition(self, job):
if job.JobType.Id is self.WINDFARMS_JOB_TYPE:
self._Event_1007(job)
else:
self._Event_1010(job)
# _NewJobStateTransition - End
# Проводит задание через процесс закрытия этапов.
# param job
def Transition(self, job):
if not isinstance(job, Job):
raise Exception("Transition must be called with an object of type Job")
if job.Id == 0:
self._NewJobStateTransition(job)
elif job.JobState == None:
raise Exception("Job must have a state")
# Transition - End
# Важно!!! Локатор конечного автомата контрольных пунктов завершения этапов основывается на установленной переменной machine
machine = Customer1StateMachine()