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()