Русский вариант поста не исправляется, и возможно немного устарел, поэтому если вы видите явную ошибку, то гляньте английскую версию вернее всего там она исправлена. У меня просто нет времени на синхронизацию исправлений между двумя версиями.
Иногда в приложениях, нам необходимо создавать рабочие процессы исполнение которых занимает продолжительное время. Например, после создания документа отправить письмо менеджеру с ссылкой на подтверждение публикации. Это самый простой пример, в реальном мире рабочие процессы могут быть чрезвычайно сложны. В веб приложениях типичный пример это оформление покупок в интернет магазине. На рынке существуют готовые решения этой проблемы, например Microsoft Workflow Foundation. Но для некоторых задач это явный оверкил. В большинстве приложений, не требуется позволять пользователям создавать и редактировать свои процессы. Давайте же попробуем написать свое легковесное решение и для начала определим список требований.
Для начала определим классы активностей.
public class Action
{
}
//показать текст пользователю
public class Show : Action
{
public string What {
get;
set;
}
}
//показать текст пользователю и спросить значение определенного типа
public class Ask<T> : Action
{
public string What {
get;
set;
}
}Action это базовый класс для всех активностей. Ask и Show просто два примера конкретных активностей. Пока все просто. Теперь определим стратегию сохранения процессов.
public class Storage
{
static JsonSerializerSettings settings;
static Storage ()
{
settings = new JsonSerializerSettings ();
}
public static void Save<T> (T wrkfl, String name)
{
var json = JsonConvert.SerializeObject (wrkfl, settings);
File.WriteAllText (name, json);
}
public static T Load<T> (string name)
where T:new()
{
if (File.Exists (name)) {
var json = File.ReadAllText (name);
return JsonConvert.DeserializeObject<T> (json, settings);
}
return new T ();
}
}Очень простой вспомогательный класс, который сериализует объекты в json и сохраняет в текстовый файл и также делает обратную операцию. Теперь определим класс процесса и шага процесса. Вернее всего мы будем использовать процессы примерно так:
public class Unit//no result object
{
Unit ()
{
}
public static Unit Value = new Unit ();
}
public class WorkflowStep<T>
{
public WorkflowStep (Action act)
{
...
}
public WorkflowStep (T value)
{
...
}
public Action Action {
get;
private set;
}
public bool IsExecuted ()
{
...
}
public T GetValue ()
{
...
}
}
public abstract class Workflow<TB>
{
protected WorkflowStep<T> Do<T> (Action act)
{
var step = new WorkflowStep<T> (act);
return step;
}
protected WorkflowStep<T> Ask<T> (String what)
{
return Do<T> (new Ask<T> (){ What = what });
}
protected WorkflowStep<Unit> Show (String what)
{
return Do<Unit> (new Show (){ What = what });
}
public abstract WorkflowStep<TB> GetResult ();
}
public class SumWorkflow:Workflow<int>
{
public SumWorkflow ()
{
}
// не корректный код
public override WorkflowStep<int> GetResult ()
{
var a = Ask<int> ("enter a");
var b = Ask<int> ("enter b");
var res = a + b;
Show ("Result= " + res);
return res;
}
}
class MainClass
{
public static void Main (string[] args)
{
var fileName = "workflow.json";
var wrkfl = Storage.Load<SumWorkflow> (fileName);
Console.WriteLine ("Current workflow state");
...
var res = wrkfl.GetResult ();
if (res.IsExecuted ()) {
Console.WriteLine ("Finished");
File.Delete (fileName);
} else {
if (res.Action is Show) {
Console.WriteLine ((res.Action as Show).What);
wrkfl.AddResult (Unit.Value);
} else if (res.Action is Ask<int>) {
Console.WriteLine ((res.Action as Ask<int>).What);
var resp = Console.ReadLine ();
var val = int.Parse (resp);
wrkfl.AddResult (val);
}
Storage.Save (wrkfl, fileName);
}
}
}Метод GetResult это суть нашего решения, но в данный момент он не работает. Давайте посмотрим на этот метод внимательней и опишем поведение каждой строки.
var a = Ask<int> ("enter a");
//проверить если уже существует результат выполнения активности
//присвоить его переменной "а" и продолжить выполнение,
//если результата нет то прервать выполнение и вернуть необходимую
//активность. Эта линия не зависит от ранее вычисленных результатов.
var b = Ask<int> ("enter b");
//Поведение тоже, но строка зависит от предыдущего результата "a"
//(мы не хотим исполнять эти строки параллельно).
var res = a + b;
Show ("Result= " + res);
//Первая строка должна быть выполнена как обычный код c#.
//Мы можем добавить ее ко второй "Show" строке и рассматривать вместе.
//Поведение тоже как и у активности Ask, но зависит от Tuple(a,b)
return res;
//просто вернуть результат, маркировать его как вычисленныйТеперь мы можем выразить это в реальном коде.
var a = Ask<int> ("enter a");
if (!a.IsExecuted ()) {
return new WorkflowStep<WORKFLOWRESTYPE> (a.Action);
}
var aValue = a.GetValue ();
var b = Ask<int> ("enter b");
if (!b.IsExecuted ()) {
return new WorkflowStep<WORKFLOWRESTYPE> (b.Action);
}
var bValue = b.GetValue ();
var res = aValue + bValue;
var c = Show ("Result= " + res);
if (!c.IsExecuted ()) {
return new WorkflowStep<WORKFLOWRESTYPE> (c.Action);
}
return new WorkflowStep<WORKFLOWRESTYPE> (res){IsExecuted = true};;Мы можем убрать код возвращения в специальную функцию return, но мы не можем убрать “if .. return ..” в какую либо функцию. Решением будет переписать код в стиле продолжений.
public class WorkflowStep<T>
{
...
public WorkflowStep<TB> Bind<TB> (Func<T,WorkflowStep<TB>> f)
{
if (this.IsExecuted ()) {
return f (this.GetValue ());
}
return new WorkflowStep<TB> (this.Action, this.Context, this.Index);
}
public static WorkflowStep<TB> Return<TB> (TB x)
{
return new WorkflowStep<TB> (x);
}
}
public class SumWorkflow:Workflow<int>
{
public override WorkflowStep<int> GetResult ()
{
Ask<int> ("enter a").Bind(
a=> Ask<int> ("enter b").Bind(
b=>{
var res = a + b;
return Show ("Result= " + res).Bind(
unit => unit.Return(res)
);
}
)
);
}
}Теперь все компилиться, но это не выглядит как обычная функция c#. Можем ли мы сделать это лучше? Определенно да, наши функции return и bind это паттерн монады и мы можем, как и прежде, добавить синтаксический сахар linq.
public static class WorkflowMonad
{
public static WorkflowStep<T> Return<T> (this T value)
{
return new WorkflowStep<T> (value);
}
public static WorkflowStep<U> Bind<T, U> (
this WorkflowStep<T> m,
Func<T, WorkflowStep<U>> k)
{
if (m.IsExecuted ()) {
return k (m.GetValue ());
}
return new WorkflowStep<U> (m.Action);
}
public static WorkflowStep<V> SelectMany<T, U, V> (
this WorkflowStep<T> id,
Func<T, WorkflowStep<U>> k,
Func<T, U, V> s)
{
return id.Bind (x => k (x).Bind (y => s (x, y).Return ()));
}
public static WorkflowStep<B> Select<A, B> (
this WorkflowStep<A> a,
Func<A, B> select)
{
return a.Bind (aval => WorkflowMonad.Return (select (aval)));
}
}Теперь мы можем переписать нашу функцию определения процесса.
public override WorkflowStep<int> GetResult ()
{
return from a in Ask<int> ("enter a")
from b in Ask<int> ("enter b")
let res = a + b
from u in Show ("Result= " + res)
select res;
}Мы все еще не знаем как написать функции IsExecuted и GetResult класса WokflowStep. Мы должны где-то сохранять результаты уже выполненных активностей. Мы можем инкрементно присваивать номера на каждое создание активностей и использовать List
public class ExecutionContext
{
public ExecutionContext ()
{
Memory = new List<string> ();
}
public List<string> Memory {
get;
set;
}
public int Index {
get;
private set;
}
public void Restart ()
{
Index = 0;
}
public void Inc ()
{
Index = Index + 1;
}
}
public static class SerializationHelpers
{
public static T ParseValue<T> (this string json)
{
return JsonConvert.DeserializeObject<T> (json);
}
public static string ValueToString<T> (this T value)
{
return JsonConvert.SerializeObject (value);
}
}Теперь мы должны добавить контекст в классы Workflow и WokflowStep.
public class WorkflowStep<T>
{
public WorkflowStep (Action act, ExecutionContext context, int index)
{
Action = act;
Context = context;
Index = index;
}
public WorkflowStep (T value)
{
Context.Memory.Add (value.ValueToString ());
}
public ExecutionContext Context = new ExecutionContext ();
public bool IsExecuted ()
{
return (this.Index) < Context.Memory.Count;
}
public T GetValue ()
{
return Context.Memory [this.Index].ParseValue<T> ();
}
public int Index {
get;
set;
}
}
public abstract class Workflow<TB>
{
public Workflow ()
{
Context = new ExecutionContext ();
}
public Workflow (ExecutionContext ctx)
{
Context = ctx;
IsSubWorkflow = true;
}
public bool IsSubWorkflow {
get;
set;
}
public ExecutionContext Context {
get;
set;
}
protected WorkflowStep<T> Do<T> (Action act)
{
var step = new WorkflowStep<T> (act, Context, Context.Index);
Context.Inc ();
return step;
}
protected WorkflowStep<T> Ask<T> (String what)
{
return Do<T> (new Ask<T> (){ What = what });
}
protected WorkflowStep<Unit> Show (String what)
{
return Do<Unit> (new Show (){ What = what });
}
public abstract WorkflowStep<TB> GetResultInt ();
public WorkflowStep<TB> GetResult ()
{
if (!IsSubWorkflow)
Context.Restart ();
return GetResultInt ();
}
public void AddResult (object val)
{
Context.Memory.Add (val.ValueToString ());
}
}Сделано. Теперь мы можем выразить композицию процессов.
public class WorkflowComposition:Workflow<int>
{
public override WorkflowStep<int> GetResultInt ()
{
return from a in new SumWorkflow (Context).GetResult ()
from b in new SumWorkflow (Context).GetResult ()
let res = a + b
from v in Show ("Result of two = " + (a + b))
select a + b;
}
}Чтож, похоже мы смогли удовлетворить всем нашим требованиям из списка. Теперь мы можем использовать этот движок в консольных приложениях, asp.net сайтах или даже интегрировать в SharePoint. И паттерн монады помог нам построить достаточно изящьное решение. Мы также можем добавить поддержку для циклов, условий(как), паралллельного исполнения, транзакций и многого другого. Полный код для этого поста. P.S. Просто вообразите возможность запускать вычисления поверх баз данных или веб сервисов без неудобностей типа IRepository, IService и т.п.